不加volatile单例代码:
public class Singleton {
private static Singleton s;
private Singleton(){};
public static Singleton getInstance() { //1
if(s == null) { //2
synchronized (Singleton.class) { //3
if(s == null) { //4
s = new Singleton(); //5
}
}
}
return s; //6
}
}
在并发情况下,如果没有volatile关键字,在第5行会出现问题
对于第5行 s = new Singleton(); //5
可以分解为3个步骤:
- 分配内存 相当于c的malloc
- 初始化对象
- 设置s指向刚分配的地址
上面的代码在编译器运行时,可能会出现重排序 从1-2-3 排序为1-3-2
例如现在有2个线程A,B
线程A在执行第5行代码时,B线程进来,而此时A执行了 1和3,没有执行2,此时B线程判断s不为null(外层的判空) 直接返回一个未初始化的对象,就会出现问题
正确双重检验单例模式写法:
public class Singleton {
private static volatile Singleton s;
private Singleton(){};
public static Singleton getInstance() {
if(s == null) {
synchronized (Singleton.class) {
if(s == null) {
s = new Singleton();
}
}
}
return s;
}
}
总结:加上volatile就是为了防止产生指令的重排序问题
为什么要两次判空?
1、第一个 if(instance==null),只有instance为null的时候,才进入synchronized的代码段大大减少了加锁的几率。
2、第二个if(instance==null),是为了防止可能出现多个实例的情况。
解释:当A与B同时调用getSingleton时,判断第一个if都为空,这时A拿到锁,进行第二层if判断,条件成立new了一个对象;
B在外层等待,A创建完成,释放锁,B拿到锁,进行第二层if判断,条件不成立,结束释放锁。C调用getSingleton时第一层判断不成立,直接拿到singleton对象返回,避免进入锁,减少性能开销。
网友评论