public class Singletion {
//私有构造方法
private Singletion(){};
private static Singletion instance;
public static Singletion getInstance(){
//判断对象是否为空
if (instance==null){
//此时可能并发进来多个线程。加锁
synchronized(Singletion.class){
//在进行判断是否为空,不为空,直接返回
if (instance==null){
Singletion singletion = new Singletion();
}
}
}
return instance;
}
}
看似没有问题,其实不然,出现空指针的原因是因为用new创建对象时并不是一个原子操作,使用java-c可以快速查看字节码
// 创建对象实例,分配内存
0: new #5 // class com/query/Singleton
// 复制栈顶地址,并再将其压入栈顶
3: dup
// 调用构造器方法,初始化 Cache 对象
4: invokespecial #6 // Method "<init>":()V
// 存入局部方法变量表
7: astore_1
从字节码可以看到创建一个对象实例,可以分为三步:
- 分配对象内存
- 调用构造器方法,执行初始化
- 将对象引用赋值给变量。
虚拟机实际运行时,以上指令可能发生重排序。以上代码 2,3 可能发生重排序,但是并不会重排序 1 的顺序。也就是说 1 这个指令都需要先执行,因为 2,3 指令需要依托 1 指令执行结果。
找了个图方便大家理解
上面错误双重检查锁定的示例代码中,如果线程 1 获取到锁进入创建对象实例,这个时候发生了指令重排序。当线程1 执行到 t3 时刻,线程 2 刚好进入,由于此时对象已经不为 Null,所以线程 2 可以自由访问该对象。然后该对象还未初始化,所以线程 2 访问时将会发生异常。