本文目录导读:

- 推荐:静态内部类(Initialization-on-demand Holder)
- 最安全:使用枚举(Effective Java 作者推荐)
- 经典方式:双重检查锁定(DCL)
- 简单但较耗性能:饿汉式
- 不推荐:同步方法
- 总结与选择建议
保证多线程环境下单例模式的安全,核心思路是确保实例化过程是原子性的、可见性的,并且禁止指令重排序。
以下是几种常见的实现方式,按推荐程度从高到低排序:
推荐:静态内部类(Initialization-on-demand Holder)
这是最优雅、最高效的方式,它利用了 Java 类加载机制的延迟加载和线程安全特性。
原理:
- 外部类被加载时,内部类
SingletonHolder不会被加载。 - 只有调用
getInstance()时,内部类才会被加载,JVM 会保证INSTANCE的初始化和赋值是线程安全的。 - 类加载机制天然避免了同步开销。
public class Singleton {
// 私有构造函数,防止外部实例化
private Singleton() {}
// 静态内部类
private static class SingletonHolder {
// final 确保内存可见性
private static final Singleton INSTANCE = new Singleton();
}
// 公共的获取实例方法
public static Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
优点: 高性能、线程安全、延迟加载、代码简洁。 缺点: 无法在构造函数中传递参数(如果有这个需求,需要考虑其他方式)。
最安全:使用枚举(Effective Java 作者推荐)
Java 枚举本身就是天然的单例,JVM 保证了其线程安全和防反射攻击。
原理:
- 枚举的构造方法默认是私有的。
- JVM 在加载枚举类时,会以线程安全的方式创建定义的枚举常量。
- 枚举序列化时,反序列化不会创建新对象(普通单例类需要额外实现
readResolve方法)。
public enum Singleton {
INSTANCE;
// 可以添加其他方法
public void doSomething() {
System.out.println("Doing something safely in multi-thread!");
}
}
// 使用方式:
Singleton.INSTANCE.doSomething();
优点: 极其简单、绝对线程安全、防反射、防序列化破坏。 缺点: 不是“懒加载”(但如果枚举很小,通常可以忽略);不够“面向对象”(有时不太习惯)。
经典方式:双重检查锁定(DCL)
适用于需要延迟加载且无法使用静态内部类或枚举的场景。必须搭配 volatile 关键字。
原理:
- 第一次检查: 如果实例已存在,直接返回,避免同步开销。
- 同步块: 只有第一次创建时,多个线程可能进入此块。
- 第二次检查: 防止在等待锁的过程中,另一个线程已经创建了实例。
volatile: 禁止指令重排序。new Singleton()操作不是原子的(分配内存 -> 初始化对象 -> 指向引用),如果不加volatile,另一个线程可能看到一个未完全初始化的对象。
public class Singleton {
// 关键点:volatile 禁止指令重排序
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;
}
}
优点: 延迟加载、性能在实例创建后接近无锁。
缺点: 代码稍复杂,volatile 会略微降低性能(相对于静态内部类),且容易因为忘记加 volatile 而出 bug。
简单但较耗性能:饿汉式
在类加载时就创建实例,JVM 保证了类加载过程的线程安全。
原理: 利用 JVM 类加载机制,无论是否使用,都会创建实例。
public class Singleton {
// 类加载时就创建
private static final Singleton INSTANCE = new Singleton();
private Singleton() {}
public static Singleton getInstance() {
return INSTANCE;
}
}
优点: 实现简单,无需处理复杂同步。 缺点: 不是懒加载,如果创建开销很大或可能不被用到,会浪费资源。
不推荐:同步方法
给整个 getInstance 方法加 synchronized。
public class Singleton {
private static Singleton instance;
private Singleton() {}
// 方法级同步,性能极差
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
原因: 每次调用 getInstance() 都会获得锁,即使实例已经存在,在高并发场景下,性能会严重下降。
总结与选择建议
| 实现方式 | 线程安全 | 懒加载 | 性能 | 推荐指数 | 适用场景 |
|---|---|---|---|---|---|
| 静态内部类 | ✅ 完美 | ✅ 是 | ⭐⭐⭐ 最高 | 首选 | 大部分通用场景 |
| 枚举 | ✅ 完美 | ❌ 否(预加载) | ⭐⭐⭐ 最高 | 首选 | 绝对安全和简单的场景 |
| 双重检查锁定 (DCL) | ✅ 需配合volatile |
✅ 是 | ⭐⭐ 高 | 次选 | 需要延迟加载且无法用内部类的场景 |
| 饿汉式 | ✅ 天然 | ❌ 否 | ⭐⭐⭐ 最高 | 接受 | 实例轻量、肯定会用到的场景 |
| 同步方法 | ✅ | ✅ 是 | ❌ 极差 | 不推荐 | 仅学习用途 |
如果你的脚本是线程安全的,通常意味着需要显式处理同步,但很多“脚本”语言(如 Python、JavaScript)的单例实现方式与 Java 不同。
Python 示例(线程安全):
import threading
class Singleton:
_instance = None
_lock = threading.Lock()
def __new__(cls, *args, **kwargs):
# 双重检查锁定(Python中也可以)
if cls._instance is None:
with cls._lock:
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
JavaScript(Node.js)注意:
JS 是单线程事件循环,通常不需要担心多线程并发问题(除非用了 Worker Threads),标准模块导入(import / require)本身就是单例且安全的。