说来惭愧,下面的代码,我一直以为是线程安全的,直到昨天使用Jenkins对项目做静态代码分析的时候,发现其将这种写法标为红色醒目的bug。
// 非线程安全版本
public final class Singleton {
private static Singleton INSTANCE = null;
public static Singleton getInstance() {
if (INSTANCE == null) {
synchronized (Singleton.class) {
if (INSTANCE == null) {
INSTANCE = new Singleton();
}
}
}
return INSTANCE;
}
}
导致线程不安全的根源出在INSTANCE = new Singleton();
这一行上。这并非是一个原子操作,事实上在JVM中这句话大概做了下面3件事情:
- 给INSTANCE 分配内存
- 调用Singleton的构造函数初始化成员变量
- 将INSTANCE 对象指向分配的内存空间(执行完后INSTANCE 就非null了)
但是在JVM的即时编译器中存在指令重排序的优化。上面第2步和第3步的执行顺序不能保证。可能Singleton的构造函数初始化还未完成或者未执行,就已将INSTANCE的实例指向了未完全初始化的Singleton对象。在多线程运行中,一个线程正在进行初始化INSTANCE的成员变量,另一个线程可能就已经开始使用其成员变量了,从而导致crash或者其他异常出现。
解决办法,给INSTANCE
实例加上volatile
关键字。
// 线程安全版本
public final class Singleton {
private static volatile Singleton INSTANCE = null;
public static Singleton getInstance() {
if (INSTANCE == null) {
synchronized (Singleton.class) {
if (INSTANCE == null) {
INSTANCE = new Singleton();
}
}
}
return INSTANCE;
}
}
在这里,volatile关键字的作用是禁止指令重排序。在volatile变量的赋值操作后面有一个内存屏障,读操作不会被重排到内存屏障之前。
注意,Java 5之前的版本使用volatile的双检查还是有问题。其原因是 Java 5 以前的 JMM (Java 内存模型)是存在缺陷的,即时将变量声明成 volatile 也不能完全避免重排序,主要是 volatile 变量前后的代码仍然存在重排序问题。这个 volatile 屏蔽重排序的问题在 Java 5 中才得以修复,所以在这之后才可以放心使用 volatile。
事实上,以上版本还可以做性能优化提升。
// 性能更好的线程安全版本
public final class Singleton {
private static Singleton INSTANCE = null;
public static Singleton getInstance() {
Singleton temp = INSTANCE;
if (temp == null) {
synchronized (Singleton.class) {
temp = INSTANCE;
if (temp == null) {
INSTANCE = temp = new Singleton();
}
}
}
return temp;
}
}
使用中间变量temp
来存储INSTANCE
,其作用是在INSTANCE
字段已经初始化的情况(大部分情况),由volatile
修饰的INSTANCE
字段只需要读取一次(注意是return temp
而不是return INSTANCE
)。这种写法,性能可以提升25%
。具体可以参见wiki。
正确使用双检查还是挺麻烦的,所以呢,个人推荐使用下面的静态内部类来保证线程安全性。
// 线程安全
public final class Singleton {
public static Singleton getInstance() {
return Holder.INSTANCE;
}
private static final class Holder {
private static final Singleton INSTANCE = new Singleton();
}
}
参考资料:
- https://en.wikipedia.org/wiki/Double-checked_locking#Usage_in_Java
- http://wuchong.me/blog/2014/08/28/how-to-correctly-write-singleton-pattern/
- http://www.blogjava.net/kenzhh/archive/2016/05/16/357824.html