欢迎您访问 最编程 本站为您分享编程语言代码,编程技术文章!
您现在的位置是: 首页

【Java多线程案例】单例模式-3. 单例模式的线程安全问题

最编程 2024-07-31 20:50:56
...

3.1 问题引入

上述关于"单例模式"的介绍只是序幕,毕竟我们本章重点论述的还是多线程主题,下面就有一个重要问题了:上述我们编写的单例模式代码是否存在 线程安全问题 呢?

  • 饿汉模式:由于饿汉模式中创建唯一实例的时机在类加载的时候,而调用getInstance方法执行的过程中直接返回该唯一实例,是纯粹的读取操作,不涉及多个线程修改同一变量的情况,因此天然就是线程安全的!
  • 懒汉模式:想必聪明的小伙伴已经想到了,懒汉模式是不是就是线程不安全的呢?就是这样!下面我们就来考虑多个线程同时执行的情况

演示懒汉模式的线程安全问题
image.png
如图所示:线程t1首先进入条件判断当前实例为null,然后进入if语句块中,但是此时还没有来得及执行new操作创建实例就被调度出CPU,此时线程t2进行判断当前实例为null,然后进入if语句块中创建实例完成返回,此时线程t1重新被CPU调度,接着执行new SingletonLazy()方法,因此创建出了多个实例,即懒汉式单例模式线程不安全!!!

3.2 解决懒汉式线程安全问题

那么如何改进上述代码让"懒汉式"单例模式变成线程安全的呢?出现问题的关键在于if条件判断和new创建实例并不是原子操作,因此解决方法就是通过synchronized关键字将这两个操作打包成原子操作!

3.2.1 改进代码(解决线程安全问题)

/**
 * 改进版本单例模式(解决线程安全问题)
 */
class SingletonLazyImprove01 {
    private static SingletonLazyImprove01 instance = null; // 唯一实例
    private static Object locker = new Object();

    public static SingletonLazyImprove01 getInstance() {
        synchronized (locker) {
            if (instance == null) {
                instance = new SingletonLazyImprove01();
            }
        }
        return instance;
    }

    private SingletonLazyImprove01() {};
}

上述代码我们引入synchronized关键字将if条件判断与new创建实例操作加锁打包成"原子"操作,此时如果继续按照刚才的场景,线程t1先进行加锁,如果t2也尝试进入if语句块就会因为锁竞争而阻塞等待,直到线程t1创建完实例之后释放锁,此时t2线程判断实例已经被创建好,就直接返回,因此不会出现线程安全问题了!

3.2.2 改进代码(解决效率问题)

但是问题还没有结束!现在的代码虽然解决了线程安全问题,但是还存在着效率问题,因为我们这里调用getInstance都会先尝试加锁,然后判断当前实例是否为空,然后再解锁!但是线程安全问题只存在于第一次尝试创建实例的时候,如果实例已经创建完毕,后续所有的操作都是读取操作,就不会再产生线程安全问题了!所以我们引入的解决方式就是 if双重校验锁 ,其代码如下:

class SingletonLazyImprove02 {
    private static SingletonLazyImprove02 instance = null;
    private static Object locker = new Object();

    public static SingletonLazyImprove02 getInstance() {
        if (instance == null) {
            synchronized (locker) {
                if (instance == null) {
                    instance = new SingletonLazyImprove02();
                }
            }
        }
        return instance;
    }

    private SingletonLazyImprove02() {};
}

此时我们会看到有两个if判断语句,换做是单线程情况下我们从来不会写这样的代码,但是一旦涉及到多线程的时候,if双重校验锁是很常见的,其中第一个if语句用来判断是否需要进行加锁操作,第二个if语句用来判断是否需要创建实例,但是恰巧两个if语句的条件一样!

3.2.3 改进代码(解决指令重排序问题)

但是上述代码还有一些小问题,我们需要先介绍有关 指令重排序 的话题

指令重排序:指令重排序是一种编译器优化手段,会在保证逻辑不变的情况下调整原有代码的执行顺序,来提高程序的效率,但是有可能会引发线程安全问题
例如在创建实例的代码中:instance = new SingletonLazyImprove02();内部包括三个核心步骤

  1. 申请一块内存空间
  2. 调用构造方法对内存空间进行初始化
  3. 将内存空间地址赋值给instance变量

正常情况下执行顺序都是按照1->2->3进行的,但是编译器也可能会优化为1->3->2的步骤来执行,这两种在单线程环境下都没有问题,但是如果在多线程情况下执行就有可能出现线程安全问题
image.png
如图所示,线程t1先进行加锁,然后创建实例过程中先执行第一步与第三步,此时被调度出CPU,但是后来的线程判断实例是否为空,此时直接返回未被初始化的内存空间地址,这时就会出现错误!!!

volatile关键字:我们在之前的章节已经提到过使用volatile关键字可以解决内存可见性和指令重排序的问题,因此该问题的解决方式就是使用volatile关键字,强制让编译器完全按照1->2->3的顺序来执行

/**
 * 懒汉式单例模式改进版本(解决指令重排序)
 */
class SingletonLazyImprove03 {
    private static volatile SingletonLazyImprove03 instance = null;
    private static Object locker = new Object();

    public static SingletonLazyImprove03 getInstance() {
        if (instance == null) {
            synchronized (locker) {
                if (instance == null) {
                    instance = new SingletonLazyImprove03();
                }
            }
        }
        return instance;
    }

    private SingletonLazyImprove03() {};
}

推荐阅读