Java多线程技术五——单例模式与多线程

1 概述

本章的知识点非常重要。在单例模式与多线程技术相结合的过程中,我们能发现很多以前从未考虑过的问题。这些不良的程序设计如果应用在商业项目中将会带来非常大的麻烦。本章的案例也充分说明,线程与某些技术相结合中,我们要考虑的事情会更多。在学习本章的过程中,我们只需要考虑一件事情,那就是:如果使单例模式与多线程结合时是安全、正确的。

2 单例模式与多线程

在标准的23个设计模式中,单例模式在应用中是比较常见的。但多数常规的该模式教学资料并没有结合多线程技术进行介绍,这就造成在使用结合多线程的单例模式时会出现一些意外。

3 立即加载/饿汉模式

立即加载指的是,使用类的时候已经将对象创建完毕。常见的实现办法就是new实例化,也被称为"饿汉模式"。

java 复制代码
public class MyObject {
    //立即加载方法 == 饿汉模式
    private static MyObject object = new MyObject();
    private MyObject(){

    }
    public static MyObject getInstance(){
        return object;
    }

}
java 复制代码
public class MyThread extends Thread{
    @Override
    public void run(){
        System.out.println(MyObject.getInstance().hashCode());
    }
}
java 复制代码
public class Run1 {
    public static void main(String[] args) {
        MyThread t1 = new MyThread();
        MyThread t2 = new MyThread();
        MyThread t3 = new MyThread();
        t1.start();
        t2.start();
        t3.start();
    }
}

控制台打印的hashcode是同一个值,说明对象是一个,也就实现了立即加载型单例模式。此代码为立即加载模式,缺点是不能有其他实例变量,因为getInstance()方法没有同步,所以有可能出现非线程安全问题。

4 延迟加载/懒汉模式

延迟加载就是调用get()方法时,实例才被创建。常见的实现办法就是在get()方法中进行new实例化,也被称为"懒汉模式"。

4.1 延迟加载解析

先看下面一段代码。

java 复制代码
public class MyObject {
    private static MyObject object;

    public MyObject() {
    }
    public static MyObject getInstance(){
        if(object == null){
            object = new MyObject();
        }
        return object;
    }
}
java 复制代码
public class MyThread extends Thread{
    @Override
    public void  run(){
        System.out.println(MyObject.getInstance().hashCode());
    }
}
java 复制代码
public class Run1 {
    public static void main(String[] args) {
        MyThread t1 = new MyThread();
        MyThread t2 = new MyThread();
        MyThread t3 = new MyThread();
        t1.start();
        t2.start();
        t3.start();
    }
}

4.2 延迟加载的缺点

前面两个实验虽然使用"立即加载"和"延迟加载"实现了单例模式,但在多线程环境中,"延迟加载"示例中的代码完全是错误的,根本不能保持单例的状态。下面来看如何在多线程环境中结合错误的单例模式创建出多个实例的。

java 复制代码
public class MyObject {
    private static MyObject object;

    public MyObject() {
    }
    public static MyObject getInstance(){
        try {
            if(object == null){
                //模拟在创建对象之前做一些准备工作
                Thread.sleep(3000);
                object = new MyObject();
            }
        }catch (InterruptedException e){
            e.printStackTrace();
        }
        return object;
    }
}
java 复制代码
public class MyThread extends Thread{
    @Override
    public void run(){
        System.out.println(MyObject.getInstance().hashCode());
    }
}
java 复制代码
public class Run1 {
    public static void main(String[] args) {
        MyThread t1 = new MyThread();
        MyThread t2 = new MyThread();
        MyThread t3 = new MyThread();
        t1.start();
        t2.start();
        t3.start();
    }
}

控制台打印3个不同的hashCode,说明创建了3个对象,并不是单例的。这就是"错误的单例模式",如何解决呢?

4.3 延迟加载的解决方案

(1)声明synchronzied关键字

既然多个线程可以同时进入getInstance()方法,只需要对getInstance()方法声明synchronzied关键字即可。修改MyObject.java类。

java 复制代码
public class MyObject {
    private static MyObject object;

    public MyObject() {
    }
    synchronized public static MyObject getInstance(){
        try {
            if(object == null){
                //模拟在创建对象之前做一些准备工作
                Thread.sleep(3000);
                object = new MyObject();
            }
        }catch (InterruptedException e){
            e.printStackTrace();
        }
        return object;
    }
}

此方法在加入同步synchronzied关键字后得到相同实例的对象,但运行效率很低。下一个线程想要取得 对象,必须等待上一个线程释放完锁之后,才可以执行。那换成同步代码块可以解决吗?

(2)尝试同步代码块

修改MyObject.java类。

java 复制代码
public class MyObject {
    private static MyObject object;

    public MyObject() {
    }
    public static MyObject getInstance(){
        try {
            synchronized (MyObject.class){
                if(object == null){
                    //模拟在创建对象之前做一些准备工作
                    Thread.sleep(3000);
                    object = new MyObject();
                }
            }

        }catch (InterruptedException e){
            e.printStackTrace();
        }
        return object;
    }
}

此方法加入同步synchronzied语句块后得到相同实例对象,但运行效率也非常低,和synchronzied同步方法一样是同步运行的。下面继续更改代码,尝试解决这个问题。

(3)针对某个重要的代码进行单独的同步。

修改MyObject.java类。

java 复制代码
public class MyObject {
    private static MyObject object;

    public MyObject() {
    }
    public static MyObject getInstance(){
        try {

                if(object == null){
                    //模拟在创建对象之前做一些准备工作
                    Thread.sleep(3000);
                    synchronized (MyObject.class) {
                        object = new MyObject();
                    }
                }


        }catch (InterruptedException e){
            e.printStackTrace();
        }
        return object;
    }
}

此方法使同步synchronzied语句块只对实例化对象的关键代码进行同步。从语句的结构上讲,运行效率却是得到了提升,但遇到多线程的情况还是无法得到同一个实例对象。

(4)使用DCL双检查锁机制

java 复制代码
public class MyObject {
    private  volatile static MyObject object;

    public MyObject() {
    }
    public static MyObject getInstance(){
        try {
            if(object == null){
                Thread.sleep(2000);
                synchronized (MyObject.class){
                    if(object == null){
                        object = new MyObject();
                    }
                }
            }
        }catch (InterruptedException e){
            e.printStackTrace();
        }
        return object;
    }
}

使用volatile修改变量object,使该变量在多个线程间可见,另外禁止 object = new MyObject()代码重排序。object = new MyObject()包含3个步骤:

1、memory = allocate();//分配对象的内存空间

2、ctorInstance(memory);//初始化对象

3、object = memory;//设置instance指向刚分配的内存地址

JIT编译器有可能将这三个步骤重新排序。

1、memory = allocate();//分配对象的内存空间

2、object = memory;//设置instance指向刚分配的内存地址

3、ctorInstance(memory);//初始化对象

这时,构造方法虽然还没有执行,但object对象已具有内存地址,即值不是null。当访问object对象中的值时,是当前声明数据类型的默认值。

创建线程类MyThread.java代码如下。

java 复制代码
public class MyThread extends Thread{
    @Override
    public void run(){
        System.out.println(MyObject.getInstance().hashCode());
    }
}
java 复制代码
public class Run1 {
    public static void main(String[] args) {
        MyThread t1 = new MyThread();
        MyThread t2 = new MyThread();
        MyThread t3 = new MyThread();
        t1.start();
        t2.start();
        t3.start();
    }
}

可见,使用DCL双检查锁成功解决了懒汉模式下的多线程问题。DCL也是大多数多线程结合单例模式使用的解决方案。

5 使用静态内置类实现单例模式

DCL可以解决多线程单例模式的非线程安全问题。还可以使用其他办法达到同样的效果。

java 复制代码
public class MyObject {
    private static class MyObjectHandler{
        private static MyObject object = new MyObject();
    }

    public MyObject() {
    }
    public static MyObject getInstance(){
        return MyObjectHandler.object;
    }
}
java 复制代码
public class MyThread extends Thread{
    @Override
    public void run(){
        System.out.println(MyObject.getInstance().hashCode());
    }
}
java 复制代码
public class Run1 {
    public static void main(String[] args) {
        MyThread t1 = new MyThread();
        MyThread t2 = new MyThread();
        MyThread t3 = new MyThread();
        t1.start();
        t2.start();
        t3.start();
    }
}

6 使用static代码块实现单例模式

静态代码块中的代码在使用类的时候就已经执行,所以可以使用静态代码块的这个特性实现单例模式。

java 复制代码
public class MyObject {
    private static MyObject object = null;

    public MyObject() {
    }
    static {
        object = new MyObject();
    }
    public static MyObject getInstance(){
        return  object;
    }
}
java 复制代码
public class MyThread extends Thread{
    @Override
    public void run(){
        for (int i = 0; i < 5; i++) {
            System.out.println(MyObject.getInstance().hashCode());
        }
    }
}
java 复制代码
public class Run1 {
    public static void main(String[] args) {
        MyThread t1 = new MyThread();
        MyThread t2 = new MyThread();
        MyThread t3 = new MyThread();
        t1.start();
        t2.start();
        t3.start();
    }
}
相关推荐
十八度的天空1 分钟前
第01节 Python的基础语法
开发语言·python
谈笑也风生1 分钟前
经典算法题型之排序算法(三)
java·算法·排序算法
yue00812 分钟前
C# 字符串倒序
开发语言·c#
自己的九又四分之三站台18 分钟前
导入数据到OG GraphQL以及创建graph
java·后端·graphql
强子感冒了20 分钟前
Java学习笔记:String、StringBuilder与StringBuffer
java·开发语言·笔记·学习
低保和光头哪个先来26 分钟前
场景6:对浏览器内核的理解
开发语言·前端·javascript·vue.js·前端框架
小北方城市网29 分钟前
Python + 前后端全栈进阶课程(共 10 节|完整版递进式|从技术深化→项目落地→就业进阶,无缝衔接基础课)
大数据·开发语言·网络·python·数据库架构
程序员JerrySUN35 分钟前
OP-TEE + YOLOv8:从“加密权重”到“内存中解密并推理”的完整实战记录
android·java·开发语言·redis·yolo·架构
阿里嘎多学长1 小时前
2025-12-30 GitHub 热点项目精选
开发语言·程序员·github·代码托管
+VX:Fegn08951 小时前
计算机毕业设计|基于springboot + vueOA工程项目管理系统(源码+数据库+文档)
java·数据库·vue.js·spring boot·后端·课程设计