今天在回顾单例模式时,我们都知道懒汉式单例中有一种叫做双重检查锁的单例模式。
我们来看下下面的代码有没有问题:
这段代码我们可以看到,即优化了性能,在多线程情况下,如果实例不为空了,则直接返回了。这样就不用等待排队获取锁了。
同时也保证了线程的安全性,即全程只会出现一个实例。
但是真的没有问题了吗?我们来分析一下:
在执行到 lazyDoubleCheckSingleton = new LazyDoubleCheckSingleton();
这条语句时,这里是会发生一个指令重排序的问题的。什么是指令重排序呢?
正常的我们创建一个对象,对于底层来说是: a. 内存分配 b. 初始化 c. 返回对象引用
但是由于指令重排序:我们的指令执行过程可能会成为 a. 内存分配 b. 返回对象引用 c. 初始化
如果是我们指令重排序的这种结果。那我们上面的代码就会产生问题了。
即:假设第一个线程执行到了 lazyDoubleCheckSingleton = new LazyDoubleCheckSingleton();
这条语句,但是只是执行了指令中的 a 、 b 即初始化还没有完成,但此时 lazyDoubleCheckSingleton
这个引用已经不为空了,此时第二个线程过来,在外层判断发现lazyDoubleCheckSingleton
不等于空,就直接返回了,但此时返回的对象很明显是个半成品,还没有初始化。因此就会导致产生不可预估的错误。
具体为什么会产生指令重排序,或者指令重排序的详细概念请看https://blog.csdn.net/weixin_37841366/article/details/113086438
以上是问题的背景。
但是我的疑问是为什么会产生指令重排序呢?我印象中学习过的JUC不是说加锁:即Synchronized是可以保证 有序性、可见性、原子性的吗?这个赋值创建的语句这不是在Synchronized代码块里面呢吗?怎么会有指令重排序的问题呢?
下面我们就来分析一下为什么会产生这样的问题吧~
首先我们需要先好好理解一下加锁保证的有序性和volatile关键字防止指令重排序保证的有序性的区别。
首先我们需要明确一点:那就是加锁是无法防止指令重排序的。那为什么说他能够保证有序性呢?
我们需要了解一个语义:
编译器和处理器必须遵守as-if-serial语义,即不管怎么重排序(编译器和处理器为了提高并行度),单线程程序的执行结果不能被改变。
因为Synchronized块中的代码相当于是单线程执行的,而因为这个语义的存在,单线程执行的执行结果是保证不能被改变的,因此Synchronized代码块包裹的代码是有序的代码。这里的有序指的是宏观的有序。
但我们的双检查单例为什么靠Synchronized锁做不到保证有序呢?
因为我们在代码块外面的那个if判断是不受Synchronized控制的。Synchronized的内部是有序了。但是外部依旧无序。
因此上面的代码我们需要添加volatile关键字防止指令重排序。让其保证微观上的强有序性。