题目
如何设计线程安全的延迟初始化并保证跨线程可见性?
信息
- 类型:问答
- 难度:⭐⭐⭐
考点
Java内存模型,happens-before原则,volatile语义,指令重排序,内存屏障
快速回答
实现线程安全的延迟初始化需同时解决原子性和可见性问题:
- 使用
volatile修饰实例变量禁止指令重排序 - 通过双重检查锁定(DCL)减少同步开销
- 利用
static final字段的初始化保证实现安全发布 - 遵循happens-before原则确保跨线程可见性
问题场景
在并发环境下实现单例模式时,经典的DCL(Double-Checked Locking)实现存在隐患:
public class Singleton {
private static Singleton instance;
public static Singleton getInstance() {
if (instance == null) { // 第一次检查
synchronized (Singleton.class) {
if (instance == null) { // 第二次检查
instance = new Singleton();
}
}
}
return instance;
}
}核心问题
上述代码在Java内存模型下可能失效:
- 指令重排序:
new Singleton()操作可能被拆分为:- 分配内存空间
- 初始化对象
- 将引用赋值给instance
- 可见性问题:未使用同步机制时,线程A的写入操作对线程B不可见
解决方案
public class Singleton {
// volatile禁止指令重排序
private static volatile Singleton instance;
public static Singleton getInstance() {
Singleton localRef = instance;
if (localRef == null) {
synchronized (Singleton.class) {
localRef = instance;
if (localRef == null) {
instance = localRef = new Singleton();
}
}
}
return localRef;
}
}关键改进点:
volatile修饰符:- 禁止JVM指令重排序(通过内存屏障实现)
- 保证写操作对后续读操作可见(遵循happens-before原则)
- 局部变量
localRef:减少volatile变量的访问次数(性能优化)
原理说明
- happens-before原则:volatile写操作先于后续volatile读操作
- 内存屏障:
- StoreStore屏障:禁止普通写与volatile写重排序
- StoreLoad屏障:禁止volatile写与后续操作重排序
- 安全发布:volatile保证对象初始化完成后才暴露引用
最佳实践
- 优先使用静态内部类实现(利用类加载机制保证线程安全):
public class Singleton { private static class Holder { static final Singleton INSTANCE = new Singleton(); } public static Singleton getInstance() { return Holder.INSTANCE; } } - Java 9+推荐使用
VarHandle实现无锁方案
常见错误
- 遗漏
volatile导致DCL失效 - 在构造函数中启动线程(可能访问未完全初始化的对象)
- 误用
final字段(需在构造函数完成前赋值)
扩展知识
- 初始化安全:final字段在构造函数完成后保证可见性(JLS 17.5)
- VarHandle:Java 9引入的替代
AtomicReferenceFieldUpdater的方案private static final VarHandle INSTANCE; static { try { INSTANCE = MethodHandles.lookup().findVarHandle( Singleton.class, "instance", Singleton.class); } catch (Exception e) { ... } } - 内存模型演进:JSR-133修复了早期Java内存模型的缺陷