java 单例模式中双重检查锁定 volatile 的作用

volatile 是保证了可见性还是有序性?

有序性:是因为 instance = new Singleton(); 不是原子操作。编译器存在指令重排,从而存在线程1 创建实例后(初始化未完成),线程2 判断对象不为空,但实际对象扔为空,造成错误。

可见性:是因为线程1 创建实例后还只存在自己线程的工作内存,未更新到主存。线程 2 判断对象为空,创建实例,从而存在多实例错误。

结论

主要是禁止重排序,初始化一个实例(SomeType st = new SomeType())在java字节码中会有4个步骤,

  1. 申请内存空间
  2. 初始化默认值(区别于构造器方法的初始化)
  3. 执行构造器方法
  4. 连接引用和实例

这4个步骤后两个有可能会重排序,1234 1243都有可能,造成未初始化完全的对象发布。volatile可以禁止指令重排序,从而避免这个问题。

为什么要禁止重排序?

确保先执行构造器方法,再将引用和实例连接到一起。如果没有禁止重排序,会导致另一个线程可能获取到尚未构造完成的对象。

为什么没有起到可见性的作用?

JSR-133

An unlock on a monitor happens before every subsequent lock on that same monitor

第二次非null判断是在加锁以后,则根据这一条,另一个线程一定能看到这个引用被赋值。所以即使没有volatile,依旧能保证可见性。

如果不加volatile,能不能使代码正确运行?

既然可见性已经有了保证,那我们只需要保证有序性。怎么保证有序性呢?

只需要在“构造对象”和“连接引用与实例”之间加上一道内存屏障

由于是在单线程里,同样根据JSR-133

Each action in a thread happens before every action in that thread that comes later in the program’s order

DoubleCheckedLocking