单例模式多线程下出问题主因是懒汉式未同步导致重复创建及JVM重排序引发未初始化对象暴露;DCL需volatile禁止重排序,静态内部类更安全轻量,ThreadLocal是线程级非全局单例。

为什么单例模式在多线程下会出问题
因为 getInstance() 方法里常见的懒汉式实现没有同步控制,多个线程可能同时通过 if (instance == null) 判断,各自创建新实例,破坏单例语义。
- 最典型的错误写法是直接在方法体里用
new Singleton()而不加锁或双重检查 - JVM 的指令重排序可能导致
instance引用被提前赋值,但对象构造尚未完成,其他线程拿到未初始化完成的对象 - 即使加了
synchronized,整个方法加锁会严重降低并发性能
public class Singleton {
private static Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) { // 线程A和B都可能在这里为true
instance = new Singleton(); // 可能发生重排序:引用先于构造完成
}
return instance;
}
}
双重检查锁定(DCL)必须配合 volatile
volatile 关键字不是可选的——它禁止指令重排序,并保证其他线程能看到 instance 的最新值。缺了它,DCL 在 JDK 5 之前几乎必然失效。
-
volatile不能防止构造函数被多次调用,但它确保“看到的引用一定是完全初始化后的” - 注意:
volatile仅对引用本身有效,不递归保护内部字段;若单例对象内部有可变状态,仍需额外同步 - DCL 适用于初始化开销大、使用频率高的场景;如果初始化快,直接用静态内部类更简洁
public class Singleton {
private static volatile Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
静态内部类方式比 DCL 更安全也更轻量
利用 JVM 类加载机制的天然线程安全性,既避免同步开销,又无需 volatile,且代码更简短清晰。
ShopWind网店系统是国内最专业的网店程序之一,采用ASP语言设计开发,速度快、性能好、安全性高。ShopWind网店购物系统提供性化的后台管理界面,标准的网上商店管理模式和强大的网店软件后台管理功能。ShopWind网店系统提供了灵活强大的模板机制,内置多套免费精美模板,同时可在后台任意更换,让您即刻快速建立不同的网店外观。同时您可以对网模板自定义设计,建立个性化网店形象。ShopWind网
- 外部类加载时,
SingletonHolder不会被加载;只有首次调用getInstance()时,JVM 才触发其初始化 - 类初始化由 JVM 保证原子性与可见性,等价于隐式加了锁,但无用户态锁开销
- 不适用于需要传参初始化的场景(比如依赖注入容器管理的单例)
public class Singleton {
private Singleton() {}
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
ThreadLocal 是线程级单例,不是全局单例
很多人误以为 ThreadLocal 是解决共享对象线程安全的“银弹”,其实它只是把共享问题转化成了每个线程持有一份副本——内存占用会上升,且生命周期管理容易出错。
立即学习“Java免费学习笔记(深入)”;
-
ThreadLocal适合保存线程上下文信息(如用户身份、事务ID),不适合缓存大对象或长期存活资源 - 在线程池场景下,线程复用会导致
ThreadLocal值残留,必须显式调用remove(),否则可能引发内存泄漏或数据污染 - 它和单例模式目标不同:一个是“每线程一个”,一个是“全局唯一”,混用会模糊设计意图
private static final ThreadLocal真正难的不是写出线程安全的单例,而是判断该不该用单例——很多所谓“工具类”根本不需要全局状态,直接用无状态静态方法更干净;而一旦涉及可变状态,哪怕用了 DCL 或静态内部类,后续修改字段时也极易引入新的竞态条件。DATE_FORMAT = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd"));










