场景
二话不说,直接上代码
public class DoubleCheckDemo {
private static DoubleCheckDemo demo;
public DoubleCheckDemo getDemo(){
if(demo == null){
synchronized (DoubleCheckDemo.class){
if(demo == null){
demo = new DoubleCheckDemo();
}
}
}
return demo;
}
}
在实际场景我们有时候需要推迟一些开销比较高的对象内存化操作。我们想出了一种看似很“”聪明“”的操作。就像我代码写的,第一次检查实例不为空,就一顿操作加锁和初始化。所以节省直接大规模synchronized 代码的开销。
1.多个线程试图一起上创建对象,只有一个能成。
2.好了之后,以后再有线程来,我们一个判断直接挡住synchronized 的后续操作。
是不是感觉很完美,
但是这是错误的!!!
但是这是错误的!!!
但是这是错误的!!!
向来,重要的话说三遍!
因为如果代码执行到
if(demo == null){
我们的demo可能还没有初始化完成!
寻根问底
其实我们这个操作。
demo = new DoubleCheckDemo()
看似很简单,但是,暗藏了3个操作:
1.首先,要创建一个对象,得分配内存空间吧。
2.有了内存空间,我们得实例化这个对象吧。
3.设置demo指向内存地址
以上其实是废话,但是必须要讲。
引出问题
再放一段代码:
{
a = 1; 、// 1
b= 2; // 2
c = a + b; // 3
}
在一个线程中,我们都认为,1肯定happens - before 2 ,2 happens - before 3,的确JMM为我们程序员保证了这个玩意。
但是,JMM也给编译器和执行器做了一个保证,如果单线程下的
执 行 结 果!!!
执 行 结 果!!!
执 行 结 果!!!
不会发生改变,你们想怎么重排序都行,于是乎,编译器觉得,应该213比较好,也不会改变结果,所以执行顺序变成了213.
回到问题
通过上面的代码合解析,同理,
1.首先,要创建一个对象,得分配内存空间吧。
2.有了内存空间,我们得初始化这个对象吧。
3.设置demo指向内存地址
这三个操作只要执行结果不变,其他的怎么排都没事,所以有好事者,排序排成了132,这个不影响结果吧。
A线程:1 3 2
B线程:在设置demo指向地址之后直接来判断了,此时demo == null返回是false的!!!
那我们应该怎么办?
1.不给23重排序
2.给排,但是不给B看到,B来的时候我排完了。类似于宋襄公半渡江而不击。我们渡河渡了一半,你很仁慈的等我们渡完了,军队排列完整再和我们开战。
方案一:不给排
给对象声明volatile声明。这个声明23之间多线程中的重排序将会被禁止。
volatile关键字尚未成文。
方案二:排!
在jvm的类的初始化阶段(Class已经被加载,但是还没被线程使用之前),会执行类的初始化。执行期间,会获得一把锁,锁住。
所以可以这么干:
public class DoubleCheckDemo {
public DoubleCheckDemo getDemo(){
return DemoSync.demo;
}
private static class DemoSync{
public static DoubleCheckDemo demo= new DoubleCheckDemo();
}
}
A线程访问的时候,得到了这把锁,线程A执行初始化,这时候B来了,但是我这里是内部类在初始化,我获得的是这个初始化类的锁,你B能看到我重排序吗?你B再怎么也要先获得我这个类初始化的锁吧,但是A在持有。B只能眼睁睁看着。等A一顿操作完了,值也上了,之前
1.首先,要创建一个对象,得分配内存空间吧。
2.有了内存空间,我们得初始化这个对象吧。
3.设置demo指向内存地址
也做完了,B才拿到,这就没有任何问题了。
总结
直接双重检查锁和延迟初始化是不行的!!!
直接双重检查锁和延迟初始化是不行的!!!
直接双重检查锁和延迟初始化是不行的!!!
至少是不严谨的。所以我们可以通过两种方式来避免一切引起问题的可能性,把问题扼杀在摇篮中!