AI分析不同阶层思维 七:DCL 单例不加 volatile,迟早出事故
·
一、事故现场还原
业务场景:配置中心客户端单例
系统启动后,需要加载一份配置,并在全系统范围内复用。为了避免启动慢,同时又不想每次都加锁,团队采用了经典的“双重检查锁”单例写法。
问题代码
public class ConfigManager {
private static ConfigManager instance; // 没有 volatile
private Map<String, String> config;
private ConfigManager() {
this.config = loadFromRemote();
}
public static ConfigManager getInstance() {
if (instance == null) {
synchronized (ConfigManager.class) {
if (instance == null) {
instance = new ConfigManager();
}
}
}
return instance;
}
public String get(String key) {
return config.get(key);
}
}
线上偶发问题:
instance不为 nullconfig为 null- 调用
get()直接 NPE - 重启后问题消失
代码没有任何显式并发写操作,但系统表现得像“内存被污染”。
二、时间线:问题是如何必然发生的
核心问题只有一句话:
对象创建不是原子操作。
new ConfigManager() 在 JVM 层面至少分三步:
- 分配内存
- 执行构造方法
- 将引用赋值给 instance
在没有 volatile 的情况下,这三步允许指令重排序。
| 时间点 | 线程 A(创建实例) | 线程 B(获取实例) | instance 状态 | config 状态 |
|---|---|---|---|---|
| t1 | 分配对象内存 | null | 未初始化 | |
| t2 | instance = 内存地址 | 非 null | 仍为 null | |
| t3 | 执行构造方法 | 非 null | 初始化中 | |
| t4 | 读取 instance | 非 null | null | |
| t5 | 构造完成 | 非 null | 正常 |
关键点在这里:
- 第二次
if (instance == null)只能防止重复创建 - 它防不住“看到一个没构造完的对象”
- 只要发生一次指令重排,这个问题就是确定性存在的
这不是概率问题,只是是否被触发的问题。
三、薪资分水岭:不同段位的工程师如何应对
| 分析维度 | 月薪1-2万工程师 | 月薪3-5万工程师 | 年薪100万+工程师 |
|---|---|---|---|
| 问题定位 | 怀疑业务逻辑 | 怀疑并发可见性 | 直接锁定 DCL |
| 根因分析 | 单例写错了 | new 不是原子操作 | 指令重排 + 发布 |
| 解决方案 | 方法整体加锁 | 给 instance 加 volatile | 替换单例实现 |
| 考虑范围 | 当前类 | JVM 内存模型 | 系统级规范 |
| 后续动作 | 修完上线 | 补并发说明 | 禁止 DCL 写法 |
差距不在“会不会写 DCL”,而在是否理解 DCL 成立的前提条件。
四、思维差异的底层逻辑
认知深度差异
-
月薪1-2万
- 认为 synchronized 已经解决并发
- 停留在代码结构层
-
月薪3-5万
- 理解 new 的执行步骤
- 知道 volatile 禁止重排
-
年薪100万+
- 关注对象发布模型
- 优先选择更稳妥的设计
风险识别能力
-
月薪1-2万
- 只能在 NPE 出现后排查
-
月薪3-5万
- 能提前识别“半初始化对象”风险
-
年薪100万+
-
一眼识别:
- DCL
- 懒加载
- 手写并发控制
-
抽象能力差异
-
月薪1-2万
- 修:这个单例有 bug
-
月薪3-5万
- 抽象:DCL 必须配合 volatile
-
年薪100万+
-
上升为原则:
- 能不用 DCL 就不用
- 单例优先交给 JVM
-
五、可操作的升级路径
给月薪1-2万工程师的刻意练习
-
练习:拆解 new 的执行过程
- 写下
new Object()的三步 - 在纸上模拟两个线程的交错执行
- 标出“对象引用先于构造完成”的时刻
- 写下
给月薪3-5万工程师的突破挑战
-
挑战:替换 DCL 单例
- 使用静态内部类
- 使用枚举单例
- 对比三种方式的初始化时机和并发语义
public class Singleton {
private Singleton() {}
private static class Holder {
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return Holder.INSTANCE;
}
}
六、总结
双重检查锁不是错,错的是在不了解前提的情况下使用它。volatile 不是为了可见性,而是为了禁止对象发布时的指令重排。
薪资差异的本质,不是 API 熟练度,而是是否真正理解:
并发问题,90% 出在“看不见的那一步”。
更多推荐




所有评论(0)