前文《单例模式》中,提到了双锁检测DCL,简单回顾下DCL双锁检测这种实现方式,有以下两个重点:
- 同步代码块内再次进行了一次判空校验
- 静态变量使用了volatile进行修饰
本文的目的主要就是解决上次遗留的问题,为啥使用volatile关键字修饰静态变量,这其中隐藏着不少知识点。下面贴一下相关代码:
public class Singleton {
private volatile static Singleton singleton;
public static Singleton getInstance() {
//检查实例,如果不存在,就进入同步代码块
if (singleton == null) {
//竞争锁 原子操作
synchronized (Singleton.class) {
//进入同步代码块后,再检查一次,如果仍是null,才创建实例
if (singleton == null) {
singleton = new Singleton();//!!!可能会出现指令重排
}
}
}
return singleton;
}
}
先给出结论:使用volatile关键字修饰singleton,为了避免出现指令重排导致实际创建出的singleton不可用
singleton = new Singleton()这行代码在字节码层面由多行指令构成,主要是以下三步:
- 首先在堆中为Singleton对象分配内存
- 进行Singleton对象的初始化操作
- 将引用变量singleton指向堆中的Singleton对象
假设此时有两个线程,线程1先执行到这段代码,进入同步代码块中,进行实例的创建,执行到singleton = new Singleton()的时候发生了指令重排,并没有先进行对象的初始化工作,而是先将引用变量singleton指向堆中的singleton对象,当线程1还没进行singleton对象的初始化操作时进行了上下文切换;此时线程2开始执行,在第一步的if (singleton == null)判断出singleton实例不为null,则直接使用这个实例,但是此时的singleton对象由于没进行初始化,所以此时的singleton变量仅仅是个半成品,这就是问题所在。
通过volatile去修饰变量singleton,可以避免指令重排,volatile基于内存屏障能够阻止指令重排,保证代码都是按序执行,避免了上面的问题。但是这里仔细想想,singleton = new Singleton()这段代码是在同步代码块中执行的,synchronized关键字应该也是可以保证指令的有序性:
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
难道synchronized关键字不能保证指令的有序执行?之前看的那些八股都白看了?显然是这其中应该有哪些知识点没有掌握,首先回顾下synchronized和volatile的特点:
- Synchoronized关键字能够保证操作的原子性,指令的有序性,数据的可见性
- Volatile关键字能够保证指令的有序性、数据的可见性,不能保证操作的原子性
其实Synchoronized保证指令的有序性是个描述错觉,其并不能真正阻止指令的重排,不像Volatile那样能够在指令层面阻止乱序,只是因为代码都在同一个同步代码块中,就算指令被重排序了,然后线程经历了上下文切换,其他线程来执行代码块,但是由于获取不到锁,这些线程无法执行相应的操作,最终还是由原线程继续执行完剩余代码,最终的效果是一样的。
public class Singleton {
private volatile static Singleton singleton;
public static Singleton getInstance() {
//检查实例,如果不存在,就进入同步代码块
if (singleton == null) {
//竞争锁 原子操作
synchronized (Singleton.class) {
//进入同步代码块后,再检查一次,如果仍是null,才创建实例
if (singleton == null) {
singleton = new Singleton();//!!!可能会出现指令重排
}
}
}
return singleton;
}
}
回过头在看这段代码,可以发现singleton变量并没有被synchronized完全管理,还有if (singleton == null)这行代码,其在同步代码块之外,因此使用volatile关键字进行了修饰。
如果想利用synchronized保证代码有序性,则必须确保涉及到的代码都完全被synchronized管理才行,否则还是得使用volatile关键字。