双重检查锁定需用volatile修饰实例变量,否则因指令重排序可能导致其他线程看到未初始化对象;正确写法为volatile + 两次null检查 + 同步块;替代方案如静态内部类更简洁安全。

Java中双重检查锁定(Double-Checked Locking)可用于实现线程安全的延迟初始化,但要“完美”实现,关键在于正确使用volatile关键字——否则存在指令重排序导致其他线程看到未完全构造的对象,这是经典陷阱。
为什么需要volatile
在没有volatile修饰的情况下,JVM可能将对象的构造过程(分配内存→初始化字段→设置引用)重排序为“分配内存→设置引用→初始化字段”。此时另一个线程可能拿到一个非null但字段尚未初始化的实例,引发空指针或逻辑错误。
加上volatile后,它禁止了对这个变量的读写重排序,并保证其写操作对所有线程可见,从而确保安全发布。
标准写法:带volatile的双重检查
以单例为例,这是被广泛验证的正确实现:
public class Singleton {
private static volatile Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) { // 第一次检查(无锁)
synchronized (Singleton.class) {
if (instance == null) { // 第二次检查(加锁后)
instance = new Singleton(); // 关键:volatile保证此赋值的可见性与有序性
}
}
}
return instance;
}
}
常见错误与规避要点
- 漏掉volatile:这是最常见错误,会导致不可预测的并发问题,即使测试通过也可能在高并发或不同JVM上失败。
- 同步块锁对象不一致:必须用同一个静态锁(如类对象),避免多个锁导致检查失效。
-
在构造函数中泄露this:若构造过程中把
this发布到其他线程(如注册监听器、启动线程),即使加volatile也无法阻止未初始化状态暴露。 -
用于非无参构造或依赖注入场景需谨慎:双重检查只适用于对象创建本身无外部依赖、且构造过程不涉及复杂初始化逻辑的情况;否则建议用
java.util.concurrent.ConcurrentHashMap或Holder模式替代。
替代方案对比
如果只是单例,推荐更简洁安全的静态内部类方式(利用类加载机制保证线程安全和延迟加载):
public class Singleton {
private Singleton() {}
private static class Holder {
static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return Holder.INSTANCE;
}
}
它无需同步、无volatile、无重排序风险,且同样延迟加载。双重检查更适合需要参数化初始化或运行时决定是否创建的场景(如带配置的资源实例)。
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











