为什么需要线程安全的单例?
在单线程环境下,一个简单的单例模式就能完美工作,但在多线程环境下,如果多个线程同时调用单例的获取方法(通常是 getInstance()),可能会创建出多个实例,这就破坏了单例的唯一性。
问题场景: 假设一个非线程安全的单例类:
public class Singleton {
private static Singleton instance;
private Singleton() {} // 私有构造函数
public static Singleton getInstance() {
if (instance == null) { // 线程A和线程B可能同时在这里判断为true
instance = new Singleton(); // 然后各自创建一个实例
}
return instance;
}
}
当线程A执行到 if (instance == null) 时,它判断为 true,但还没来得及执行 instance = new Singleton(),此时CPU时间片用完,线程B开始执行,线程B同样判断 if (instance == null),也为 true,于是线程B也创建了一个新的实例,这样就产生了两个不同的实例,单例模式失效。
synchronized 解决方案
为了解决上述问题,最直接的想法就是给 getInstance() 方法加上 synchronized 关键字,确保同一时间只有一个线程能进入这个方法。
同步整个方法(简单但低效)
这是最原始的 synchronized 单例实现。
public class Singleton {
private static Singleton instance;
private Singleton() {
// 防止外部通过反射创建实例
if (instance != null) {
throw new RuntimeException("Use getInstance() method to get the single instance.");
}
}
// 整个方法被 synchronized 修饰
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
工作原理:

synchronized关键字确保了在任何时刻,只有一个线程可以执行getInstance()方法。- 当线程A进入该方法并创建实例后,线程B、C等再想调用
getInstance()时,必须等待线程A执行完毕。 - 这样,
instance == null的判断和实例的创建过程是原子性的,避免了重复创建的问题。
优点:
- 实现简单,易于理解。
- 线程安全,能保证单例的唯一性。
缺点:
- 性能开销大,无论实例是否已经创建,每次调用
getInstance()都需要进行同步,同步操作(获取锁、释放锁)是比较耗时的,这会严重影响性能,尤其是在高并发场景下,当单例对象已经创建后,后续的调用仍然在同步等待,这是不必要的。
同步代码块(优化版)
为了解决方案一的性能问题,我们可以只同步“创建实例”这一小段代码,而不是整个方法。
public class Singleton {
private static Singleton instance;
private Singleton() { /* ... */ }
public static Singleton getInstance() {
if (instance == null) { // 第一次检查
synchronized (Singleton.class) {
if (instance == null) { // 第二次检查
instance = new Singleton();
}
}
}
return instance;
}
}
这种写法被称为 “双重检查锁定”(Double-Checked Locking, DCL)。
工作原理:

- 第一次检查 (
if (instance == null)):- 大多数情况下,单例实例已经存在,线程调用
getInstance()时,直接通过第一个if判断,返回实例,完全不需要进入同步块,性能很高。
- 大多数情况下,单例实例已经存在,线程调用
- 进入同步块 (
synchronized (Singleton.class)):- 只有当
instance为null时,线程才会尝试获取锁,这大大减少了同步的开销。
- 只有当
- 第二次检查 (
if (instance == null)):- 当线程A获取锁进入同步块后,可能会发生线程B在队列中等待,当线程A执行完毕释放锁后,线程B进入同步块,线程B需要再次检查
instance是否为null,因为线程A可能已经创建了实例,如果线程B不检查,它也会再次创建一个实例,导致重复创建。
- 当线程A获取锁进入同步块后,可能会发生线程B在队列中等待,当线程A执行完毕释放锁后,线程B进入同步块,线程B需要再次检查
优点:
- 性能高,只有在第一次创建实例时需要同步,后续调用几乎是无锁的,性能接近于非线程安全的版本。
- 线程安全,通过双重检查,确保了只有一个实例被创建。
必须使用 volatile 关键字!
这是一个至关重要的点。在 Java 1.4 及更早版本中,DCL 是有缺陷的,不能正常工作。 从 Java 5 开始,volatile 关键字的语义得到了增强,可以解决这个问题。
为什么需要 volatile?
问题出在 instance = new Singleton(); 这行代码,它并非一个原子操作,大致可以分解为三个步骤:
- 分配对象的内存空间。
- 初始化对象。
- 将
instance引用指向分配的内存地址。
在多线程环境下,由于指令重排序,执行顺序可能是 1 -> 3 -> 2。
- 线程A 执行了 1 和 3,但还没执行 2。
instance已经不为null了,但它是一个未完全初始化的对象。 - 线程B 调用
getInstance(),进行第一次检查if (instance == null),由于instance已经指向了内存地址(步骤3已完成),判断为false,于是直接返回了这个未初始化的instance。 - 线程B 得到了一个不完整的、有问题的对象,导致程序出错。
volatile 关键字可以禁止指令重排序,确保 instance = new Singleton(); 的三个步骤按 1->2->3 的顺序完成,从而避免了上述问题。

其他更优的单例实现方式
虽然 synchronized 是经典实现,但在现代 Java 开发中,有更简洁、更高效的单例模式实现方式。
静态内部类(推荐)
这种方法利用了 JVM 类加载机制来保证线程安全,是推荐使用的方式之一。
public class Singleton {
// 私有构造函数
private Singleton() {}
// 静态内部类
private static class SingletonHolder {
// 静态实例,由 JVM 在加载 SingletonHolder 类时初始化
private static final Singleton INSTANCE = new Singleton();
}
// 提供公共的静态获取方法
public static Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
工作原理:
Singleton类的加载和初始化并不会立即加载SingletonHolder类。- 只有当
getInstance()方法第一次被调用时,JVM 才会去加载SingletonHolder类,并初始化其静态变量INSTANCE。 - JVM 的类加载机制是线程安全的,
INSTANCE的初始化过程也是线程安全的。 - 这种方式既保证了线程安全,又没有带来任何同步开销,是一种完美的实现。
枚举(最推荐)
这是由 Effective Java 的作者 Joshua Bloch 提出的“最佳单例实现方式”,它不仅能避免多线程同步问题,还能防止通过反射和序列化/反序列化破坏单例。
public enum Singleton {
INSTANCE; // 单个枚举元素就是单例实例
// 可以添加自己的方法
public void doSomething() {
System.out.println("Singleton is doing something.");
}
}
使用方式:
Singleton singleton = Singleton.INSTANCE;
优点:
- 绝对线程安全,由 JVM 保证。
- 防止反射攻击,通过反射试图调用私有构造函数来创建枚举实例时,JVM 会直接抛出异常。
- 防止序列化破坏,枚举类型的序列化和反序列化是由 JVM 保证的,它总能保证反序列化后的对象和序列化前是同一个对象。
总结与对比
| 实现方式 | 线程安全 | 性能 | 简洁性 | 防止反射/序列化破坏 | 推荐度 |
|---|---|---|---|---|---|
synchronized 同步方法 |
是 | 低 | 简单 | 否 | ⭐⭐ |
synchronized 双重检查 |
是(需volatile) |
高 | 较复杂 | 否 | ⭐⭐⭐ |
| 静态内部类 | 是 | 极高 | 简洁 | 否 | ⭐⭐⭐⭐ |
| 枚举 | 是 | 极高 | 最简洁 | 是 | ⭐⭐⭐⭐⭐ |
- 学习和理解:
synchronized双重检查(DCL)是一个非常重要的多线程编程模式,理解其工作原理和volatile的
