public class Singleton {
private volatile static Singleton instance;
private Singleton(){}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
先看这个单例模式,这就是所谓的 DCL,这里重点不在说明什么是DCL,这里要说说,为什么 instance要用 volatile来修饰
volatile关键字有两个作用
- 保证线程可见性
- 防止指令重排序
线程可见性,即线程A修改的某个变量,线程B读取这个值,会读到修改后的值,而不是本地缓存中的值。这里缓存一致,通过两个方法实现。总线加LOCK#锁的方式(锁总线) 或者 通过缓存一致性协议(锁缓存),这两个都是硬件方面的实现。加锁的方式有性能问题,缓存一致性协议,比如MESI是这样实现的。若某对象被volatitle修改后,主内存中就把它设置为invalid状态,线程B读取时,发现是invalid状态,就从主内在中重新读取。
防止指令重排序, 是JVM层级,是通过内存屏障来实现的。并且是读写都有内存屏障
明白了这些,来说说DCL那个问题,为什么要用volatile关键字,就是防止指令重排序。1.对象创建先分配内存,2、再初始化相关数据,3、最后将引用指向堆内地址。其中第二个步骤会复杂的多,由于指令重排序的可能,最后一步可能先于第二步执行完成。
问题就来了,第一个线程进来调用,调用创建对象的方法,多线程情况下,其它进来的线程会阻塞。但创建对象的过程中,可能发生指令重排序,第3步先于第2步完成,这时候又有一个线程进来,调用第一个if决断,对象不等于null,直接反一个半初始化的对象。这就是问题,虽然发生的概率很小。加上volatile,杜绝第3步先于第2步完成的情况,这样就不可能返回一个半初始化的对象。