线程安全的单例模式是否真的安全
上次我们谈了一些关于JAVA中锁级别的问题,这一次和大家聊一聊JAVA中常见的单例模式,并且指出常见的线程安全的单例模式所存在的一些问题,并对问题给出部分解答。
首先,单例模式是我们常用和常见的经典设计模式之一,但是由于在JAVA中,多线程问题往往会引起单例模式的水土不服,所以,我们就见到了线程安全的单例模式,首先一种单例模式如下所示:
public class SafeInitialization{
private static Instance instance;
public synchronized static Instance getInstance(){
if(instance == null){
instance = new Instance();
}
return instance;
}
}
在上述代码中,我们对getInstance进行了加锁处理,但是synchronized将导致性能开销,如果存在多个线程频繁的调用getInstance方法,那么将会导致执行性能的下降,所以又出现的一种以“双重检查锁定”为原理的线程安全的单例模式,如下所述:
public class SafeInitialization{
private static Instance instance;
public static Instance getInstance(){
if(instance == null){
synchronized (SafeInitialization.class){
if(instance == null){
instance = new Instance();
}
}
}
return instance;
}
}
如代码所示:如果第一次检查对象不为null,则直接返回对象引用,就不需要执行下面的加锁操作,就可以大大的减小性能损耗,这也是我们常见的线程安全的单例模式,但是其实事实是上面的优化是错误的。根本原因发生在对象创建过程中:
instance = new Instance();
为什么这里会有错误呢,原因是这样的:
首先,一个对象的创建需要经历一下三步:
memory = allcoate(); //1.分配对象的内存空间
initInstance(memory);//2.初始化对象
instance = memory; //3.设置指针instance指向内存空间
其实如果上面三步顺序执行将不会出现问题,但是由于处理器和编译器存在重排序优化(具体内容会在之后的博客中做阐述),所以将会导致多线程之间的instance引用同步问题。比如下面一种情况就是重排序问题:
memory = allcoate(); //1.分配对象的内存空间
instance = memory; //3.设置指针instance指向内存空间,注意这时对象并未被初始化
initInstance(memory);//2.初始化对象
所以当线程B来访问:if(instance == null)
时,线程B拿到的是一个不为null的instance引用,所以会直接返回,当线程B使用instance引用做处理时,就会出现问题。
那么我们有没有什么方法去解决这个问题呢,答案当然是有的,今天我们只阐述原理较为简单的一种,第二种留待之后讲解。
其实修改很简单,如下:
public class SafeInitialization{
private volatile static Instance instance;
public static Instance getInstance(){
if(instance == null){
synchronized (SafeInitialization.class){
if(instance == null){
instance = new Instance();
}
}
}
return instance;
}
}
只需要对instance加volatile锁定就可以了,可以这样理解initInstance(memory);
是在对memory 进行写操作,instance = memory;
是在对memory进行读操作,由于对象使用volatile进行了修饰,编译器和处理器对于volatile对象的先写后读是不允许进行重排序的,所以相当于是禁止了重排序问题,重排序问题解决之后就不会出现上面的问题了。所以就可以实现绝对安全的重排序。
事实上,上面的问题出现的几率并不大,因为这种重排序只会在少数的一些JIT编译器上发生,所以作为一个程序员,真的是必须不断学习啊。。。。。。