在双重检查锁单例模式中,不加 volatile 关键字可能导致线程安全问题,具体问题场景如下:
1. 指令重排序导致对象未完全初始化
当 instance = new Singleton() 时,JVM 会执行以下步骤:
分配内存空间
初始化对象(调用构造方法)
将引用指向内存地址
无 volatile 时:
编译器和 CPU 可能对指令进行重排序,可能导致步骤 3 先于步骤 2 执行。此时,其他线程在第一次判空检查 if (instance == null) 时,可能获取到一个 未完成初始化 的 instance 对象。
示例场景:
线程 A 执行 instance = new Singleton(),但指令重排序导致引用已赋值(步骤3),对象尚未初始化(步骤2未完成)。
线程 B 进入 getInstance(),判断 instance != null,直接返回未初始化的对象并使用,导致程序异常。
2. 可见性问题(早期 Java 版本)
在 JDK 5 之前,Java 内存模型(JMM)未强制要求同步块内的变量修改对其他线程立即可见。即使线程 A 在同步块中完成实例初始化,若不加 volatile,线程 B 可能因本地缓存未刷新,仍认为 instance 为 null,从而导致重复创建实例。
示例场景:
线程 A 在同步块中创建 instance 并退出同步块,但修改未及时刷新到主内存。
线程 B 进入同步块前的第一次检查 if (instance == null),因本地缓存未更新仍判断为 null,进入同步块创建第二个实例。
总结
场景 问题描述 解决方案
指令重排序 其他线程获取到未初始化的对象实例 volatile 禁止指令重排序
可见性问题(早期JVM) 多线程环境下实例状态未及时同步,导致重复创建 volatile 强制内存可见性
代码验证
public class Singleton { private static Singleton instance; // 未加 volatile private Singleton() {} public static Singleton getInstance() { if (instance == null) { synchronized (Singleton.class) { if (instance == null) { instance = new Singleton(); // 可能导致未初始化对象被提前暴露 } } } return instance; } }
结论:
在并发环境下,必须使用 volatile 修饰单例实例,以解决指令重排序和可见性问题。
0条评论
点击登录参与评论