在我们写单例的时候,我们可能会有如下写法:
public class Test{
private static Instance instance;
public synchronized static Instance getInstance(){
if(instance == null){ //1
instance= new Instance(); //2
}
return instance;
}
}
上面是一个线程安全的单例模式,没有问题,但是我们每次都是把整个方法锁死,那么对于频繁调用,性能不是很好。
于是,我们来优化一下。
public class Test{
private static Instance instance;
public static Instance getInstance(){
if(instance == null){
synchronized(Test.class){
if(instance == null){ //1
instance= new Instance(); //2
}
}
}
return instance;
}
}
这是一种双重检验的写法,看起来非常“聪明”,如果第一次检查不为null,那么就不需要执行下面操作,可以给性能带来提升。
但是我们深入的去看看这个写法:
在创建对象的过程中,首先1.加载class对象,2.然后分配内存,3.初始化对象,4.最后添加引用。
但是在一些JIT的编译器上,3,4两个步骤可能会被重排序的(可以被从排序的前提是不影响程序执行结果,在这里并没有影响执行结果)。
那么如果线程A在初始化对象的过程被重排许了,先把引用给了A,那么线程B去拿共享变量,当B线程去读取共享变量的数据时发现为空,产生错误。那么这种情况也是不允许存在的。
那么我们要怎么解决这个问题呢?
那我们既然知道这个问题是由指令重排序引起的,那么我们就以重排序入手。
1.让程序不进行重排序。
2.使重排序对其他线程不可见。
第一种解决方案就是禁止指令重排序,即使用我们的volatile关键字。
public class Test{
private volatile static Instance instance;
public static Instance getInstance(){
if(instance == null){
synchronized(Test.class){
if(instance == null){ //1
instance= new Instance(); //2
}
}
}
return instance;
}
}
第二种解决方案就是使重排序对其他县城不可见,使用静态内部内的方式。
public class Test{
private static class InstanceHolder{
public static Instance instance = new Instance();
}
public static Instance getInstance(){
return InstanceHolder.instance;
}
}
个人比较推荐第二种方式。当然单例模式还有其他写法。就不一一举例了。