双重检查锁必须加volatile以禁止重排序并保证可见性,否则jdk5前可能产生未初始化完成的对象;holder模式利用类加载机制实现无同步延迟初始化。

双重检查锁为什么必须加 volatile?
不加 volatile 的双重检查锁在 JDK 5 之前是错的,JVM 可能重排序对象初始化步骤,导致其他线程看到未构造完成的实例。加了 volatile 后,既禁止指令重排,又保证可见性。
常见错误现象:NullPointerException 或字段为默认值(如 0、null),尤其在高并发压测时偶发出现。
-
volatile不影响性能——现代 JVM 对它的优化已很成熟,别信“它很慢”的老说法 - 必须对 instance 字段加
volatile,不能只靠 synchronized 块保护 - 构造函数里不要启动线程、注册回调或调用可被子类重写的方法,否则可能暴露
this引用
Holder 模式怎么绕过同步开销?
利用 Java 类加载机制的天然线程安全特性:静态内部类 Holder 在首次主动使用时才被加载,而类初始化由 JVM 保证原子性,无需手动同步。
使用场景:单例字段初始化逻辑较重(如加载配置、连接数据库),且确定只在首次访问时触发。
立即学习“Java免费学习笔记(深入)”;
- 必须把延迟初始化字段放在静态内部类里,例如
private static class Holder { static final Singleton INSTANCE = new Singleton(); } - 外部类不能有其他静态字段依赖该实例,否则可能提前触发类加载
- 不支持带参数的初始化——Holder 模式本质是无参构造 + 静态 final 字段
两种方案在字节码和 JIT 层面的区别
双重检查锁生成的字节码含 monitorenter/monitorexit,JIT 编译后可能做锁消除,但前提是你没逃逸出锁范围;Holder 模式完全不生成同步指令,初始化逻辑在 <clinit></clinit> 方法里,只执行一次。
性能影响:在低并发下差别微乎其微;高并发下,双重检查锁的第一次竞争成本略高(两次 volatile 读 + 一次锁),Holder 模式则把成本摊到类加载阶段,且不可规避。
- JDK 7+ 的类加载器对
static final字段做了额外优化,Holder 初始化实际比想象中更快 - 如果字段初始化可能抛异常,Holder 模式会卡死后续所有调用——因为
<clinit></clinit>失败后,该类永远无法再次初始化 - 某些 AOP 框架(如 Spring)代理静态方法时可能干扰 Holder 加载顺序,需实测验证
什么时候不该用这两种模式?
当字段初始化需要上下文参数、依赖注入容器管理生命周期、或本身是可变状态时,硬套双重检查锁或 Holder 模式反而增加维护成本。
容易踩的坑:把本该由 Spring 管理的 Bean 手动做成双重检查单例,结果破坏了事务代理、AOP 切面或作用域控制。
- Spring 默认单例 Bean 已经是线程安全的延迟初始化(基于
ConcurrentHashMap+ObjectMonitor),别重复造轮子 - 如果字段是
ThreadLocal或依赖当前线程上下文,Holder 模式完全不适用 - Android 开发中注意 Dalvik/ART 对静态类加载的兼容性差异,部分低版本系统对嵌套类初始化行为略有不同








