手写双重检查锁易漏volatile导致指令重排序,应优先用Enum单例、静态内部类或AtomicReference+Supplier;Supplier本身不保证线程安全,需配合原子操作确保仅初始化一次。

为什么不用 synchronized 块手写双重检查锁
因为容易漏掉 volatile 修饰符,导致指令重排序,多线程下可能返回未初始化完成的对象。JDK 8+ 提供的 LazySet 或更稳妥的 Supplier + AtomicReference 组合,比自己拼双重检查锁更可靠。
常见错误现象:NullPointerException 在首次调用后偶尔出现,尤其在高并发压测时;对象构造函数里有副作用(如注册监听器),但被多次执行。
- 手写双重检查锁必须给实例字段加
volatile,否则无效 - 构造逻辑复杂时,建议把初始化封装进独立方法,避免锁块内代码过长
- 如果只是单例场景,优先考虑
Enum单例或静态内部类,比Supplier更轻量
Supplier.get() 调用时机与线程安全边界
Supplier 本身不保证线程安全,它只负责“提供值”;真正决定是否懒加载、是否并发安全的,是调用它的容器逻辑。比如 AtomicReference 的 updateAndGet 可以做到原子性初始化,而裸调 supplier.get() 没有任何同步保障。
使用场景:配置对象、数据库连接池、大型缓存计算结果等启动慢、使用频次低的资源。
立即学习“Java免费学习笔记(深入)”;
- 不要直接在 getter 中反复调用
supplier.get(),这会失去懒加载意义 - 若需多次获取同一实例,应缓存返回值,例如用
AtomicReference或ConcurrentHashMap存储 - 注意
Supplier实现中抛出异常的情况:第一次调用失败后,后续调用仍会重试,可能造成重复错误
用 AtomicReference 实现线程安全的懒加载
这是最常用也最可控的方式:利用 AtomicReference.compareAndSet(null, newValue) 的原子性,确保仅一次初始化成功,其余线程直接读取已设值。
性能影响:无锁路径下几乎零开销;竞争激烈时会有少量 CAS 失败重试,但远低于 synchronized 块的上下文切换成本。
private final AtomicReference<ExpensiveObject> instance = new AtomicReference<>();
private final Supplier<ExpensiveObject> supplier = () -> new ExpensiveObject();
public ExpensiveObject getInstance() {
ExpensiveObject inst = instance.get();
if (inst == null) {
inst = supplier.get();
if (!instance.compareAndSet(null, inst)) {
inst = instance.get(); // 竞争失败,取已设置的
}
}
return inst;
}
- 不能省略第二次
instance.get(),否则可能返回 null -
supplier必须是无状态、幂等的,否则并发下可能创建多个实例 - 如果初始化逻辑可能失败,建议配合
AtomicBoolean initialized+ 异常标记,避免无限重试
Spring @Lazy 和手动 Supplier 懒加载的区别
@Lazy 是 Spring 容器级的延迟代理,本质是生成 CGLIB 代理对象,首次调用真实方法时才触发 Bean 创建;而手动用 Supplier 是代码级控制,不依赖容器,适用于非 Spring 环境或需要细粒度控制的场景。
兼容性影响:Spring @Lazy 对 final 类、private 构造器支持有限;手动方式完全由你控制构造过程,灵活性更高,但也意味着要自己处理生命周期和线程安全。
-
@Lazy无法控制“初始化失败后是否重试”,手动方式可以加 fallback 或降级逻辑 - 混合使用时注意:Spring 管理的 Bean 若注入了手动懒加载对象,后者不会被 Spring 代理或管理,其销毁需自行处理
- 单元测试中,手动
Supplier更容易 mock 和替换,@Lazy需要完整启动容器才能验证行为










