当前位置: 代码迷 >> 综合 >> 1.volatile的有序性-单例模式双重检测锁为什么要用volatile?
  详细解决方案

1.volatile的有序性-单例模式双重检测锁为什么要用volatile?

热度:65   发布时间:2023-10-21 14:15:21.0

本篇文章想讲述的问题:

  1. 单例模式-双重检测锁为什么要用volatile关键字
  2. 上下代码有依赖关系时指令重排后的执行顺序

单例模式-双重检测锁为什么要用volatile关键字

先看代码:

public class Singleton {static Singleton instance;public Object o;public Singleton() {this.o = new Object();}static Singleton getInstance(){if (instance == null) {synchronized(Singleton.class) {if (instance == null) {instance = new Singleton();}}}return instance;}
}

假设现在有两个线程A和B,A现在先进入了第2个if判断并执行第14行代码对对象进行初始化。A线程进入到构造方法对对象进行初始化时,由于JVM会对指令进行重排,所以初始化的代码顺序可能是:

第一种顺序:

  1. 分配一块内存 M;
  2. 在内存 M 上初始化 Singleton 对象(对属性o进行初始化和赋值);
  3. 然后 M 的地址赋值给 instance 变量。

第二中顺序(实际上优化后的执行路径可能是这样的):

  1. 分配一块内存 M;
  2. 将 M 的地址赋值给 instance 变量;
  3. 最后在内存 M 上初始化 Singleton 对象(对属性o进行初始化和赋值)。

 假设虚拟机对指令重排优化后,按第二种顺序执行。A线程现在执行到第二步将 M 的地址赋值给 instance 变量;现在cpu对线程切换并切换到B线程上,B此时若走到第一个if判断,此时instance已不为空,所以直接返回instance(此时A线程还没有完成对instance变量引用的对象完成初始化也没有执行到第16行对对象锁进行释放)。因为此时B返回的instance的属性o没有被初化或赋值,所以B线程返回的instance对象o属性为空,如果B线程对返回的instance对象的o属性进行方法调用可能出现空指针异常。

因此解决办法:对instance加上volatile关键字。

原因:volatile可以禁止JVM和处理器对指令进行重排。

volatile并不是可以禁止所有的重排,用以下代码来解释:

    volatile int m;int a, b;long c, d;public void testVolatile() {a = 10;b = 10;m = 20;c = 30;d = 40;}

 volatile能保证的顺序是:对volatile修饰的变量进行操作的语句的前面的语句,一定比对volatile修饰的变量进行操作的语句先执行。对volatile修饰的变量进行操作的语句的后面的语句,一定比对volatile修饰的变量进行操作的语句后执行。有点绕,简单说也就是:testVolatile方法被执行时,m = 20前面两行代码一定比m = 20要先执行(不能保证a = 10; b = 10;这两句的执行先后顺序);m = 20后面两行代码一定比m = 20要后执行(不能保证c = 30; d = 40;这两句的执行先后顺序)。

这就解释了为什么单例模式的双重检测锁为什么要用volatile关键字,以及volatile关键字是如何禁止指令重排的。

上下代码有依赖关系时指令重排后的执行顺序

代码示例:

public void method() {int a = 0;a = 10;int b = 0;b = 20;}

这个method方法,看上去可能是按写的代码的顺序执行的,执行顺序是:先执行第2行,再执行第3、4、5行。但是代码执行顺序,往往会被这个编译器优化,进行指令重排序。优化后的执行顺序可能是:先执行第4、5行,再执行第2、3行。也可能是:先执行第4、2行,再执行第3、5行。还有其它执行顺序的可能。但是有一点是确定的:因为第3行依赖第2行,所以第3行一定会在第2行后面执行,也就是第2行一定比第3行先执行;同理第4行也一定比第5行先执行;不然执行就会报错我有问题。

以上是个人观点,若又不正确的地方欢迎指正。