题目
单例双检锁的实现方式为什么要用 volatile?为什么要双检?
答案
双检锁是单例模式中兼顾线程安全和性能的经典实现,但其实现细节涉及 **Java 内存模型(JMM)** 和 **指令重排序** 等底层机制。以下从设计动机、实现原理和 `volatile` 的必要性三方面详细分析。 ## 为什么要“双检”? ### 性能优化:避免无意义的锁竞争 - **第一次检查(无锁)**:当实例已存在时,直接返回,避免进入同步代码块,减少锁竞争的开销。 - **第二次检查(加锁后)**:确保即使多个线程同时通过第一次检查,只有一个线程能真正创建实例。 ```java public static DCLSingleton getInstance() { if (instance == null) { // 第一次检查:无锁快速判断 synchronized (DCLSingleton.class) { if (instance == null) { // 第二次检查:防止重复初始化 instance = new DCLSingleton(); } } } return instance; } ``` ### 线程安全:解决竞态条件 若只有单层检查,多线程环境下可能导致: - **多次初始化**:多个线程同时通过第一次检查,创建多个实例。 - **部分初始化对象**:未完成构造的对象被其他线程访问(由指令重排序导致)。 ## 为什么必须使用 `volatile`? ### 禁止指令重排序 **对象初始化的非原子性**:`instance = new DCLSingleton()` 的实际操作分为三步: ```java 1. 分配内存空间(allocate) 2. 初始化对象(init) 3. 将引用指向内存地址(assign) ``` **问题**:编译器或 CPU 可能对步骤 2 和 3 进行重排序。若线程 A 执行顺序为 1→3→2,其他线程可能在步骤 3 后(但步骤 2 未完成)访问到未初始化的对象。 `volatile` 的作用:通过插入 **内存屏障**(Memory Barrier),禁止 JVM 对 `volatile` 变量的写操作与其之前的任何读写操作重排序,确保 `assign` 在 `init` 之后执行。 ```java private static volatile DCLSingleton instance; // 禁止指令重排序 ``` ### 保证可见性 `volatile` 强制线程每次访问变量时从主内存读取最新值,而非使用本地缓存(如 CPU 寄存器或缓存行),确保其他线程能立即看到实例的创建。