【Java踩坑笔记】23_double-checkedlocking单例,你写的真的线程安全吗? 23 | double-checked locking 单例你写的真的线程安全吗摘要双重检查锁DCL是实现懒加载单例的经典写法但少了volatile就会返回半初始化的对象。本文从指令重排角度彻底讲清这个问题并给出更好的单例实现方案。一、问题现象// ❌ 不完整的 DCL 单例少了 volatilepublicclassSingleton{privatestaticSingletoninstance;// ❌ 少了 volatileprivateSingleton(){}publicstaticSingletongetInstance(){if(instancenull){// 第一次检查synchronized(Singleton.class){// 加锁if(instancenull){// 第二次检查instancenewSingleton();// ❌ 可能返回半初始化的对象}}}returninstance;}}问题instance new Singleton()不是原子操作可能发生指令重排另一个线程拿到的是半初始化的对象。二、踩坑现场场景高并发下单例对象字段为 nullpublicclassConfigManager{privatestaticConfigManagerinstance;// ❌ 没加 volatileprivatefinalMapString,Stringconfig;privateConfigManager(){configloadConfigFromDb();// 耗时操作}publicstaticConfigManagergetInstance(){if(instancenull){synchronized(ConfigManager.class){if(instancenull){instancenewConfigManager();}}}returninstance;}publicStringgetConfig(Stringkey){returnconfig.get(key);// ❌ 可能 NPEconfig 还没初始化完}}现象偶尔出现NullPointerException但无法稳定复现是生产环境最隐蔽的 bug 之一。三、原理解析3.1new Singleton()的底层步骤指令 1分配内存空间给 instance 指向这块内存 指令 2初始化对象调用构造方法给字段赋值 指令 3将 instance 指向分配的内存地址问题步骤 2 和步骤 3 可能被重排指令重排是 CPU 和编译器为了优化性能做的单线程下不影响结果。重排后指令 1分配内存空间 指令 3将 instance 指向分配的内存地址 ← 此时 instance ! null但对象还没初始化完 指令 2初始化对象3.2 另一个线程看到的情况线程 A执行 new Singleton() → 执行到指令 3instance 已非零但对象未初始化完 → 被挂起 线程 B调用 getInstance() → 第一次检查instance null→ falseinstance 已非零 → 直接 return instance但对象还没初始化完 → 使用这个半初始化的对象 → 出错3.3volatile如何修复volatile禁止指令重排privatestaticvolatileSingletoninstance;// ✅ 加 volatilepublicstaticSingletongetInstance(){if(instancenull){synchronized(Singleton.class){if(instancenull){instancenewSingleton();// ✅ volatile 禁止指令重排}}}returninstance;}volatile的 happens-before 规则对volatile变量的写操作happens-before后续对该变量的读操作换句话说instance的写步骤 3不会被重排到对象初始化步骤 2之前四、正确写法4.1 标准 DCL 单例Java 5// ✅ 正确加 volatilepublicclassSingleton{privatestaticvolatileSingletoninstance;// ✅ 必须加 volatileprivateSingleton(){}publicstaticSingletongetInstance(){if(instancenull){// 第一次检查无锁性能高synchronized(Singleton.class){if(instancenull){// 第二次检查防止多个线程同时通过第一次检查instancenewSingleton();}}}returninstance;}}4.2 枚举单例最推荐Effective Java 推荐// ✅ 最推荐枚举单例天然防止反射攻击、序列化问题publicenumSingleton{INSTANCE;// 枚举实例天然是单例publicvoiddoSomething(){// 业务逻辑}}// 使用Singleton.INSTANCE.doSomething();枚举单例的优势写法极简天然线程安全枚举实例的创建由 JVM 保证天然防止反射攻击枚举不能通过反射创建实例天然支持序列化不需要实现Serializable接口4.3 静态内部类单例推荐// ✅ 推荐静态内部类懒加载 线程安全publicclassSingleton{privateSingleton(){}privatestaticclassHolder{privatestaticfinalSingletonINSTANCEnewSingleton();}publicstaticSingletongetInstance(){returnHolder.INSTANCE;// ✅ 第一次调用时才加载 Holder 类懒加载}}原理类加载是线程安全的JVM 保证Holder类只被加载一次。4.4 简单场景直接用静态字段// ✅ 如果不需懒加载最简单publicclassSingleton{publicstaticfinalSingletonINSTANCEnewSingleton();privateSingleton(){}}五、最佳实践✅ 单例实现的方案选择方案推荐度懒加载线程安全防止反射/序列化攻击枚举单例⭐⭐⭐⭐⭐✅✅✅静态内部类⭐⭐⭐⭐✅✅❌需额外处理DCL volatile⭐⭐⭐✅✅❌需额外处理静态字段⭐⭐❌类加载时就初始化✅❌ 为什么 DCL 在 Java 1.4 及之前是坏的Java 1.5 之前volatile的语义不够强即使加了volatileDCL 依然可能返回半初始化的对象。Java 1.5 增强了volatile的语义JSR-133DCL 才变得安全。️ 阿里巴巴 Java 开发手册规约【推荐】枚举类名带上 Enum 后缀枚举成员名称需要全大写单词间用下划线隔开。【推荐】如若使用单例模式推荐使用枚举方式实现。六、小结DCL 单例少了volatile会返回半初始化的对象指令重排导致volatile禁止指令重排是 DCL 正确的关键最推荐用枚举单例Effective Java 推荐写法简单且天然安全其次是静态内部类单例懒加载 线程安全如果不需要懒加载直接用静态字段最简单下一篇预告CompletableFuture 的默认线程池生产环境慎用—— 共用ForkJoinPool.commonPool()IO 任务会拖垮整个 JVM 的异步任务。