Java中的双重检查锁(double checked locking)分析
分析通过双重检查锁来获得一个单例时,我主要从3个方面来考虑:
.
① synchronized: 通过在getInstance方法上加上同步锁,进行线程控制
② 双重if判断: 为了避免了在首层判断就加上Synchorzied同步锁,导致锁的粒度过大,导致效率的低下 ,所以采用双重if判断,在第二层判断才引入对性能开销较大的synchorzied锁,双重if判断本质上是对效率的优化
③ volatile : 另外,为了必免jvm在指令优化时,对创建对象(new)过程出现的指令重排序现象,需要组引用对象用volatile修饰
下面我们正式开始分析
相信,我们在解决并发获得单例时,第一想到的就在getInstance方法上加上同步锁。
代码如下:
public class Singleton {
private static Singleton uniqueSingleton;
private Singleton() {
}
public synchronized Singleton getInstance() {
if (null == uniqueSingleton) {
uniqueSingleton = new Singleton();
}
return uniqueSingleton;
}
}
但往往,每个线程在getInstance方法时,都要拿到同步锁,会使得系统开销增大,有点没必要。
优化方案
当已经有一个实例后,通过双重判断,可以使得以后的线程在调用getInstance方法不需要获得同步锁,效率优化。
public class Singleton {
private static Singleton uniqueSingleton;
private Singleton() {
}
public Singleton getInstance() {
if (null == uniqueSingleton) {
synchronized (Singleton.class) {
if (null == uniqueSingleton) {
uniqueSingleton = new Singleton(); // error
}
}
}
return uniqueSingleton;
}
}
但还有一个问题,在有些情况下,通过这种方式拿到的Singleton对象,可能是错误的 。
为什么呢?
回顾我们new对象的3个步骤
① 分配内存空间
② 初始化对象
③ 将对象指向刚分配的内存空间
但jvm在指令优化时,会出现步骤②和③对调的情况,比如线程1在经过俩层为null判断后,进入new的动作,在还没有初始化对象时,就返加了地址值,线程2在第一个为null判断时,因为对象已经不为空,那么就直接返回了对象。
这时,就会出现问题。
(虽然我没遇到过此问题,也不清楚这种重排序在实际应用中会导致什么,但大佬都很怕这个问题)
那么,怎么解决呢? 在引用对象前加上volatile关键字,可以解决jvm重排序问题
代码如下
public class Singleton {
private volatile static Singleton uniqueSingleton;
private Singleton() {
}
public Singleton getInstance() {
if (null == uniqueSingleton) {
synchronized (Singleton.class) {
if (null == uniqueSingleton) {
uniqueSingleton = new Singleton();
}
}
}
return uniqueSingleton;
}
}
使用volatile关键词主要可以保证代码的执行顺序不受jvm重排序影响,所以就上面的问题,对象肯定会在初始化后才会返回内存的地址值。
有关volatite的更多介绍,需要借助jvm内存模型,可以参考本人转载文章volatile关键字解析
End!
18-11-11 修正 于松江图书馆