单列模式-懒汉式中静态变量为什么需要加volatile
代码实现:
public class Single {
// 饿汉式写法
/* private static final Single single = new Single();
private Single() {}
public static Single getInstance() {
System.out.println("获取到了变量");
return single;
}*/
// 懒汉式写法
private static volatile Single single ;
private Single(){
if(single != null){
throw new RuntimeException("不允许创建多个实例");
}
}
public static Single getInstance() {
if(single == null){
synchronized (Single.class){
if(single == null){
single = new Single();
}
}
}
return single;
}
}问题根源:指令重排序
问题的核心在于 single = new Single(); 这行代码。它并不是一个原子操作,在 JVM 中实际上被分解为三个步骤:
分配内存空间:为新的
Single对象分配一块内存。初始化对象:调用构造函数,初始化对象内部的字段。
赋值引用:将
single变量指向第一步分配好的内存地址。
然而,在现代 JVM 和 CPU 架构中,为了优化性能,指令重排序(Instruction Reordering) 是允许的。只要程序的最终结果一致,JVM 和 CPU 可能会改变指令的执行顺序。
一个可能发生的重排序是:步骤 3 和步骤 2 被交换了顺序。即:
分配内存空间。
将
single变量指向内存地址(此时single不为 null,但对象还未初始化!)初始化对象。
没有 volatile 时会发生什么?
现在我们来看一下,如果没有 volatile,在多线程环境下会发生什么:
线程 A 第一次进入
getInstance(),发现single == null,获得锁,进入同步块。线程 A 再次检查
single == null,开始执行single = new Single();。由于指令重排序,线程 A 先将未完全构造好的
Single实例的地址赋值给了single变量(步骤 3先于步骤 2 执行)。此时single已经不是 null 了,但对象内部的字段可能还是默认值(0, false, null),构造函数还没有执行完毕。此时,线程 B 也调用
getInstance()。它进行第一次检查if (single == null),发现single已经不是 null 了(因为线程 A 已经执行了重排序后的步骤 3)!线程 B 于是直接返回
single所指向的那个“半成品”对象。线程 B 使用这个未完全初始化的对象,可能会导致不可预料的错误(如空指针异常、状态不一致等)。
volatile 如何解决这个问题?
当你将变量声明为 volatile 时:
java
private static volatile Single single;你做了两件非常重要的事情:
禁止指令重排序:对
volatile变量的写操作,会在其前后插入内存屏障(Memory Barrier)。这意味着JVM和CPU必须保证,在single变量被赋值之前,new Single()的构造函数必须已经完全执行完毕(即步骤 1->2->3 的顺序得到保证)。写操作之前的任何指令都不能被重排序到写操作之后。保证可见性:一个线程对
volatile变量的修改,会立即被其他线程看到。这确保了所有线程都能拿到最新的、正确的值。
加入了 volatile 关键字后,执行流程就变得安全了:
线程 A 完整地构造好
Single对象之后,才会将引用赋值给single。线程 B 在第一次检查时,要么看到
single是null,要么看到的是一个已经完全初始化好的对象,绝不会看到一个中间状态。
总结
因此,对于所有使用双重检查锁定(DCL) 实现的懒汉式单例模式,必须将单例实例声明为 volatile,以防止因指令重排序导致的诡异并发 bug。你看到的 IDE 或代码检查工具的提示是非常专业和正确的。
补充:更好的替代方案
虽然 DCL + volatile 是有效的,但在 Java 中,实现线程安全的单例还有更简洁、更被推崇的方式:
静态内部类(Holder)方式(推荐):
java
public class Single { private Single() { if (Holder.INSTANCE != null) { throw new RuntimeException("不允许创建多个实例"); } } public static Single getInstance() { return Holder.INSTANCE; } private static class Holder { private static final Single INSTANCE = new Single(); } }这种方式利用了 JVM 的类加载机制来保证初始化实例时的线程安全性,且无需同步开销,是懒加载的。
枚举(Enum)方式(最安全,防反射攻击):
java
public enum Single { INSTANCE; // 可以添加方法 public void doSomething() { // ... } }这是《Effective Java》作者 Josh Bloch 大力推荐的方式,它能绝对防止多次实例化,即使是面对复杂的序列化或者反射攻击。