下面是一个存在安全性问题的双重检索的单例模式
private static DoubleCheckSingleton doubleCheckSingleton;
private Object t;
private DoubleCheckSingleton() {
t = new Object();
}
public static DoubleCheckSingleton getInstance() {
if (doubleCheckSingleton == null) {
synchronized (DoubleCheckSingleton.class) {
// synchronized 内部是可以进行重排序的,所以在内部进行执行的时候,可能先赋值后进行初始化
// 导致出现空指针异常
if (doubleCheckSingleton == null) {
doubleCheckSingleton = new DoubleCheckSingleton();
}
}
}
return doubleCheckSingleton;
}
public Object getT(){
return t;
}
为什么说他有问题,因为new一个对象这个过程并不是一个原子的操作,他可以看成有三个步骤
- 给对象分配空间
- 初始化对象,也就是执行对象的构造方法(执行构造方法中的指令也不是原子操作,也有可能被重排序)
- 给变量赋值,也就是把对象的地址赋值给变量
既然不是原子性操作,为了优化执行效率就有可能被重排序,编译器可能会进行重排序,cpu也可能会进行重排序。
重排序之后上面的三个操作执行顺序就可能被改变,之前是1、2、3,现在就可能是1、3、2最终就可能会导致其他线程获取到没有初始化的局部变量t,因为该局部变量是在构造函数中进行的初始化,最终就会导致空指针异常!怎么解决呢?就是加volitile
可能有人会问了,为什么重排序不会排序成2、1、3
我们要,任何的重排序,都要保证单线程执行的结果不会发现改变,很明显2,3两个操作是要依赖于1操作的结果的,所以对于这样的重排序,编译器和CPU都不可能会进行这样的操作
再有一点就是,对于一个锁的解锁,一定先行发生于随后对于这个锁的加锁。按照上面的代码来说就是,如果一个线程先进入了同步代码块,然后另外一个线程被阻塞在了synchronized上面。这种情况下,是不会出现任何问题的,因为释放锁之前的任何操作,一定会先行发生于后面获取锁的操作。
但是问题在于,如果在另外一个线程还没有被阻塞在同步代码块上的时候(也就是第一个if还没有执行的时候),同步代码块内部的重排序就已经发生了,这种情况下可没有先后的保证!
final域的重排规则
写final的重排规则:
-
JMM禁止编译器把final域的写重排序到构造函数之外。
-
编译器会在final域的写之后,构造函数return之前,插入一个StoreStore屏障。这个屏障禁止处理器把final域的写重排序到构造函数之外。
也就是说:写final域的重排序规则可以确保:在对象引用为任意线程可见之前,对象的final域已经被正确初始化过了。
读final的重排规则:
-
在一个线程中,初次读对象引用与初次读该对象包含的final域,JMM禁止处理器重排序这两个操作(注意,这个规则仅仅针对处理器)。编译器会在读final域操作的前面插入一个LoadLoad屏障。
也就是说:读final域的重排序规则可以确保:在读一个对象的final域之前,一定会先读包含这个final域的对象的引用。
如果final域是引用类型,那么增加如下约束:
-
在构造函数内对一个final引用的对象的成员域的写入,与随后在构造函数外把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序。
(个人觉得基本意思也就是确保在构造函数外把这个被构造对象的引用赋值给一个引用变量之前,final域已经完全初始化并且赋值给了当前构造对象的成员域,至于初始化和赋值这两个操作则不确保先后顺序。)
final域的内存语义是在java1.5的时候进行的增强,既然有了这些内存语义,那么时候可以利用final来实现安全的双重检索机制呢?我认为是可以的,但是无从验证,因为双重检索的最大问题在于可能会导致其他线程读取到没有初始化好的对象,final利用final来修饰对象中的属性,感觉能很好的解决这个问题
private static DoubleCheckSingleton doubleCheckSingleton;
// 对单例对象内部的属性使用final修饰,利用final的内存语义
private final Object t;
private DoubleCheckSingleton() {
t = new Object();
}
public static DoubleCheckSingleton getInstance() {
if (doubleCheckSingleton == null) {
synchronized (DoubleCheckSingleton.class) {
// synchronized 内部是可以进行重排序的,所以在内部进行执行的时候,可能先赋值后进行初始化
// 导致出现空指针异常
if (doubleCheckSingleton == null) {
doubleCheckSingleton = new DoubleCheckSingleton();
}
}
}
return doubleCheckSingleton;
}
public Object getT(){
return t;
}