JUC并发编程之单例模式双重检验锁陷阱
共 5166字,需浏览 11分钟
·
2021-04-17 23:58
首先看一段代码
public class Test04 {
private static Test04 test04;
public static Test04 getInstance() {
if (null == test04) {
synchronized (Test04.class) {
if (null == test04) {
test04 = new Test04();
}
}
}
return test04;
}
public static void main(String[] args) {
Test04 instance1 = Test04.getInstance();
Test04 instance2 = Test04.getInstance();
System.out.println(instance1);
System.out.println(instance2);
if (instance1 == instance2) {
System.out.println("true");
} else {
System.out.println("false");
}
}
}
//-----输出结果
com.dream.sunny.Test04@3f99bd52
com.dream.sunny.Test04@3f99bd52
true
经过上面这段文字进行分析,这段代码似乎比较完美,程序应该是没有任何问题,恰恰在程序并发运行的过程中,种种可能都可能存在,该文就重点讲讲在并发情况下,它可能存在的潜在且致命的问题。
我这里先放上一张这段代码被编译后的字节码内容图片,方便后续的理解。
#创建一个新对象(创建 Test04 对象实例,分配内存)
19: new #3 // class com/dream/sunny/Test04
#复制栈顶部一个字长内容(复制栈顶地址,并再将其压入栈顶)[每个线程有属于自己的栈帧]
22: dup
#根据编译时类型来调用实例方法(调用构造器方法,初始化 Test04 对象)
23: invokespecial #4 // Method "<init>":()V
#将初始化后的对象赋值给静态变量
26: putstatic #2 // Field test04:Lcom/dream/sunny/Test04;
在多线程情况下,上面三个步骤可能会发生指令重排(在一些JIT编译器中),编译器或处理器会为了提高代码性能效率,而改变代码的执行顺序。
当发生重排后,步骤2对象的引用地址赋值给了变量,然后步骤3在执行对象初始化,是不是显而易见的就看见到问题存在,步骤2的引用地址是为null的,因为对象还没有被执行完初始化,就先将对象的引用地址赋值给了变量。结果后续其他线程去读取该变量直接报错,然后又无法进行初始化,那不是就很尴尬的么。
模拟两个线程创建单例的场景,如下:
针对以上情况,是否有解决方案,答案是有的,它问题出现在指令重排,我前面有文章专门提到过这个现象,为了读者方便,我这里简单说明一下指令重排是什么,具体可以查看 "JUC并发编程之Volatile关键字详解" 这篇文章
什么是指令重排序
int a = 1;
int b = 10;
int c = a * b
写volatile修饰的变量时,JMM会把本地内存中值刷新到主内存
读volatile修饰的变量时,JMM会设置本地内存无效
volatile是Java虚拟机提供的轻量级的同步机制。volatile关键字有如下两个作用
保证被volatile修饰的共享变量对所有线程总数可见的,也就是当一个线程修改了一个被volatile修饰共享变量的值,新值总是可以被其他线程立即得知。
禁止指令重排序优化。
由于 volatile 禁止对象创建时指令之间重排序,所以其他线程不会访问到一个未初始化的对象,从而保证安全性。
最终优化后的代码如下
public class Test04 {
private volatile static Test04 test04;
public static Test04 getInstance() {
if (null == test04) {
synchronized (Test04.class) {
if (null == test04) {
test04 = new Test04();
}
}
}
return test04;
}
public static void main(String[] args) {
Test04 instance1 = Test04.getInstance();
Test04 instance2 = Test04.getInstance();
System.out.println(instance1);
System.out.println(instance2);
if (instance1 == instance2) {
System.out.println("true");
} else {
System.out.println("false");
}
}
}
//-----输出结果
com.dream.sunny.Test04@3f99bd52
com.dream.sunny.Test04@3f99bd52
true
我是黎明大大,我知道我没有惊世的才华,也没有超于凡人的能力,但毕竟我还有一个不屈服,敢于选择向命运冲锋的灵魂,和一个就是伤痕累累也要义无反顾走下去的心。
如果您觉得本文对您有帮助,还请关注点赞一波,后期将不间断更新更多技术文章