双重检验锁模式为什么要使用volatile?

并发编程情况下有三个要点:操作的原子性、可见性、有序性。

volatile保证了可见性和有序性,但是并不能保证原子性。

首先看一下DCL(双重检验锁)的实现:

public class Singleton {  
    private volatile static Singleton singleton;  
    private Singleton (){}  
    public static Singleton getSingleton() {  
    if (singleton == null) {  
        synchronized (Singleton.class) {  
          if (singleton == null) {  
              singleton = new Singleton();  
          }  
        }  
    }  
    return singleton;  
    }  
}

无论是volatile修饰singleton 还是 synchronized 还是两次是否未null的判断,都是为了保证 new 操作的正常进行

所以new操作的背后到底有什么秘密的?

new 实例背后的指令

从字节码可以看到创建一个对象实例,可以分为三步:

  1. 分配对象内存
  2. 调用构造器方法,执行初始化
  3. 将对象引用赋值给变量。

虚拟机实际运行时,以上指令可能发生重排序。以上代码 2,3 可能发生重排序,但是并不会重排序 1 的顺序。也就是说 1 这个指令都需要先执行,因为 2,3 指令需要依托 1 指令执行结果。

虽然重排序并不影响单线程内的执行结果,但是在多线程的环境就带来一些问题。

 

在JIT里,可能会将2与3进行重排序,在单线程里这里并不会发生什么问题,但是在多线程情况下,会出现下面的问题

 

时间

线程A

线程B

t1

A1:分配对象的内存空间

 

t2

A2:设置singleton指向内存空间

 

t3

 

B1:判断singleton是否为空

t4

 

B2:由于singleton不为null,线程B将访问singleton引用的对象

t5

A3:初始化该对象(使得singleton不为空)

 

t6

A4:访问singleton引用的对象

 

 

A2与A3重排序后,会让线程B在B1处判断出singleton不为null,线程B接下来将访问的singleton引用的对象是一个未初始化的对象。

所以用volatile修饰 singleton来就是禁止2与3的重排序,来保证线程安全的延迟初始化。

 

参考链接:https://blog.csdn.net/OrangeRawNorthland/article/details/83788412

posted on 2020-12-20 17:02  含光Aries  阅读(353)  评论(0编辑  收藏  举报