一个简单的单例示例
单例模式可能是大家经常接触和使用的一个设计模式,你可能会这么写
public class UnsafeLazyInitiallization {
private static UnsafeLazyInitiallization instance;
private UnsafeLazyInitiallization() {
}
public static UnsafeLazyInitiallization getInstance(){
if(instance==null){ //1:A线程执行
instance=new UnsafeLazyInitiallization(); //2:B线程执行
}
return instance;
}
}
上面代码大家应该都知道,所谓的线程不安全的懒汉单例写法。在UnsafeLazyInitiallization类中,假设A线程执行代码1的同时,B线程执行代码2,此时,线程A可能看到instance引用的对象还没有初始化。
你可能会说,线程不安全,我可以对getInstance()方法做同步处理保证安全啊,比如下面这样的写法
public class SafeLazyInitiallization {
private static SafeLazyInitiallization instance;
private SafeLazyInitiallization() {
}
public synchronized static SafeLazyInitiallization getInstance(){
if(instance==null){
instance=new SafeLazyInitiallization();
}
return instance;
}
}
这样的写法是保证了线程安全,但是由于getInstance()方法做了同步处理,synchronized将导致性能开销。如getInstance()方法被多个线程频繁调用,将会导致程序执行性能的下降。反之,如果getInstance()方法不会被多个线程频繁的调用,那么这个方案将能够提供令人满意的性能。
那么,有没有更优雅的方案呢?前人的智慧是伟大的,在早期的JVM中,synchronized存在巨大的性能开销,因此,人们想出了一个“聪明”的技巧——双重检查锁定。人们通过双重检查锁定来降低同步的开销。下面来让我们看看
public class DoubleCheckedLocking { //1
private static DoubleCheckedLocking instance; //2
private DoubleCheckedLocking() {
}
public static DoubleCheckedLocking getInstance() { //3
if (instance == null) { //4:第一次检查
synchronized (DoubleCheckedLocking.class) { //5:加锁
if (instance == null) //6:第二次检查
instance = new DoubleCheckedLocking(); //7:问题的根源出在这里
} //8
} //9
return instance; //10
} //11
}
如上面代码所示,如果第一次检查instance不为null,那么就不需要执行下面的加锁和初始化操作。因此,可以大幅降低synchronized带来的性能开销。双重检查锁定看起来似乎很完美,但这是一个错误的优化!为什么呢?在线程执行到第4行,代码读取到instance不为null时,instance引用的对象有可能还没有完成初始化。在第7行创建了一个对象,这行代码可以分解为如下的3行伪代码
memory=allocate(); //1:分配对象的内存空间
ctorInstance(memory); //2:初始化对象
instance=memory; //3:设置instance指向刚分配的内存地址
上面3行代码中的2和3之间,可能会被重排序(在一些JIT编译器上,这种重排序是真实发生的,如果不了解重排序,后文JMM会详细解释)。2和3之间重排序之后的执行时序如下
memory=allocate(); //1:分配对象的内存空间
instance=memory; //3:设置instance指向刚分配的内存地址,注意此时对象还没有被初始化
ctorInstance(memory); //2:初始化对象
回到示例代码第7行,如果发生重排序,另一个并发执行的线程B就有可能在第4行判断instance不为null。线程B接下来将访问instance所引用的对象,但此时这个对象可能还没有被A线程初始化。在知晓问题发生的根源之后,我们可以想出两个办法解决
- 不允许2和3重排序
- 允许2和3重排序,但不允许其他线程“看到”这个重排序
下面就介绍这两个解决方案的具体实现
基于volatile的解决方案
对于前面的基于双重检查锁定的方案,只需要做一点小的修改,就可以实现线程安全的延迟初始化。请看下面的示例代码
public class SafeDoubleCheckedLocking {
private volatile static SafeDoubleCheckedLocking instance;
private SafeDoubleCheckedLocking() {
}
public static SafeDoubleCheckedLocking getInstance() {
if (instance == null) {
synchronized (SafeDoubleCheckedLocking.class) {
if (instance == null)
instance = new SafeDoubleCheckedLocking();//instance为volatile,现在没问题了
}
}
return instance;
}
}
当声明对象的引用为volatile后,前面伪代码谈到的2和3之间的重排序,在多线程环境中将会被禁止。