必须用volatile修饰单例实例变量,因为其能禁止指令重排序并保证可见性:若无volatile,JVM可能将new Singleton()的构造函数调用与引用赋值重排序,导致其他线程获取到未完全初始化的对象而引发空指针异常;Java 5起volatile才具备内存屏障语义,是双重检查锁定模式成立的前提。

为什么必须用 volatile 修饰单例实例变量
不加 volatile 的双重检查锁定在 Java 5 之前是失效的,即使加了 synchronized 也拦不住指令重排序。JVM 可能将 new Singleton() 拆成三步:分配内存 → 调用构造函数 → 将引用赋值给 instance。若第二步还没执行完,第三步就提前完成,另一个线程看到 instance != null 就直接返回,结果拿到一个半初始化的对象——后续调用任意非静态方法都可能触发 NullPointerException 或更隐蔽的字段默认值问题。
加上 volatile 后,它做了两件事:禁止编译器和处理器对 instance 的读写做重排序,保证对该变量的写操作对其他线程立即可见。这是整个模式成立的前提,漏掉就是埋雷。
getInstance() 里两次 if (instance == null) 各自解决什么问题
外层检查是性能开关:避免每次调用都进同步块。一旦实例创建完成,所有后续线程走这个分支直接返回,零同步开销。
内层检查是安全兜底:防止多个线程同时通过外层检查后排队进入同步块,然后依次执行 new Singleton()。只有第一个拿到锁的线程会真正创建实例,其余线程在锁释放后再次检查,发现已存在就跳过初始化。
常见错误包括:
- 只保留外层检查 → 多线程下仍可能创建多个实例
- 只保留内层检查(即整个方法加
synchronized)→ 每次调用都串行,高并发时成为瓶颈 - 把
synchronized错误地加在instance上(如synchronized(instance))→instance初始为null,会抛NullPointerException
Java 版本和类加载机制带来的兼容性注意点
该实现仅在 Java 5 及以上版本可靠。Java 5 引入了新的内存模型(JSR-133),正式赋予 volatile 内存屏障语义;旧版本即使写了 volatile 也无法阻止重排序。
另外,不要把 synchronized 锁对象换成 this 或任意实例对象——必须是类对象,即 Singleton.class。因为静态方法中没有 this,且要确保所有线程竞争的是同一把锁。
还有个容易被忽略的细节:构造函数必须私有化。否则外部仍可通过 new Singleton() 绕过控制,破坏单例契约。
比起 DCL,静态内部类为什么更值得优先考虑
如果你只是需要一个线程安全、懒加载、无同步开销的单例,static inner class 是更简洁安全的选择:
- 利用 JVM 类加载机制天然线程安全:类首次主动使用时才初始化,且由类加载器保证全局唯一性
- 无需
volatile、synchronized等关键词,代码更少,出错概率更低 - 没有指令重排序风险,也不依赖 Java 内存模型版本
但要注意:它无法支持带参数的构造初始化(比如从配置文件读取参数后构建实例),而 DCL 在同步块内可以自由加入初始化逻辑。所以真有动态初始化需求时,DCL 仍是不可替代的方案——前提是 volatile 不能漏,两次检查不能少,锁对象不能错。

















