一、案例
public class SingletonTest {
private static SingletonTest SingletonTest;
private SingletonTest() {
}
public static SingletonTest getInstance() {
if (SingletonTest == null) {
SingletonTest = new SingletonTest();
}
return SingletonTest;
}
}
这是一个懒汉式的单例实现,众所周知,因为没有相应的锁机制,这个程序是线程不安全的,实现安全的最快捷的方式是添加 synchronized
public class SingletonTest {
private static SingletonTest SingletonTest;
private SingletonTest() {
}
public static synchronized SingletonTest getInstance() {
if (SingletonTest == null) {
SingletonTest = new SingletonTest();
}
return SingletonTest;
}
}
使用synchronized之后,可以保证线程安全,但是synchronized将全部代码块锁住,这样会导致较大的性能开销,因此,人们想出了一个“聪明”的技巧:双重检查锁DCL(double checked locking)的机制实现单例。
public class SingletonTest {
private static SingletonTest SingletonTest;
private SingletonTest() {
}
public static synchronized SingletonTest getInstance() {
if (SingletonTest == null) {
synchronized (SingletonTest.class) {
if (SingletonTest == null) {
SingletonTest = new SingletonTest();
}
}
}
return SingletonTest;
}
}
如上面代码所示,如果第一次检查instance不为null,那么就不需要执行下面的加锁和初始化操作。因此可以大幅降低synchronized带来的性能开销。上面代码表面上看起来,似乎两全其美:
-
在多个线程试图在同一时间创建对象时,会通过加锁来保证只有一个线程能创建对象。
-
在对象创建好之后,执行getInstance()将不需要获取锁,直接返回已创建好的对象。
程序看起来很完美,但是这是一个不完备的优化,在线程执行到第9行代码读取到instance不为null时(第一个if),instance引用的对象有可能还没有完成初始化。
问题的根源
问题出现在创建对象的语句singleton3 = new Singleton3();
上,在java中创建一个对象并非是一个原子操作,可以被分解成三行伪代码:
//1:分配对象的内存空间
memory = allocate();
//2:初始化对象
ctorInstance(memory);
//3:设置instance指向刚分配的内存地址
instance = memory;
上面三行伪代码中的2和3之间,可能会被重排序(在一些JIT编译器中),即编译器或处理器为提高性能改变代码执行顺序,这一部分的内容稍后会详细解释,重排序之后的伪代码是这样的:
//1:分配对象的内存空间
memory = allocate();
//3:设置instance指向刚分配的内存地址
instance = memory;
//2:初始化对象
ctorInstance(memory);
在单线程程序下,重排序不会对最终结果产生影响,但是并发的情况下,可能会导致某些线程访问到未初始化的变量。
模拟一个2个线程创建单例的场景,如下表:
时间 | 线程A | 线程B |
---|---|---|
t1 | A1:分配对象内存空间 | |
t2 | A3:设置instance指向内存空间 | |
t3 | B1:判断instance是否为空 | |
t4 | B2:由于instance不为null,线程B将访问instance引用的对象 | |
t5 | A2:初始化对象 | |
t6 | A4:访问instance引用的对象 |
按照这样的顺序执行,线程B将会获得一个未初始化的对象,并且自始至终,线程B无需获取锁!
双重检查锁问题解决方案
解决方案就是大名鼎鼎的volatile关键字,对于volatile我们最深的印象是它保证了”可见性“,它的”可见性“是通过它的内存语义实现的:
-
写volatile修饰的变量时,JMM会把本地内存中值刷新到主内存
-
读volatile修饰的变量时,JMM会设置本地内存无效
public class SingletonTest {
private static volatile SingletonTest SingletonTest;
private SingletonTest() {
}
public static synchronized SingletonTest getInstance() {
if (SingletonTest == null) {
synchronized (SingletonTest.class) {
if (SingletonTest == null) {
SingletonTest = new SingletonTest();
}
}
}
return SingletonTest;
}
}