线程安全的单例模式
在Java中,实现线程安全的单例模式(Singleton Pattern)有多种方式。不同的方式在线程安全性、性能和延迟加载(Lazy Loading)方面各有侧重。
以下是Java中实现线程安全单例的4种主流且推荐的方法,按推荐程度和应用场景从前往后介绍:
1. 双重检查锁(Double-Checked Locking, DCL)- 推荐
这是面试中最常被问到的方式。它既实现了延迟加载,又保证了线程安全,且性能较高。
-
为什么需要两次
if (instance == null)?- 外层判断:是为了在单例已经创建的情况下,直接返回实例,避免每次调用都进入
synchronized同步块,从而提高性能。 - 内层判断:是为了防止多个线程同时通过了外层判断,排队进入同步块时重复创建实例。
- 外层判断:是为了在单例已经创建的情况下,直接返回实例,避免每次调用都进入
-
为什么必须加
volatile?instance = new Singleton();这句代码在JVM中不是原子操作,它分为三步:- 分配对象的内存空间。
- 初始化对象。
- 将
instance变量指向分配的内存空间。
在某些编译器下,由于指令重排,步骤可能会变成 1 -> 3 -> 2。如果线程A执行了 1 和 3,此时
instance已经不为null,但对象还未初始化。如果此时线程B调用getInstance(),会直接拿到一个半成品对象并报错。加volatile可以禁止指令重排,保证这三步按序执行。
2. 静态内部类(Static Inner Class)- 极力推荐
这种方式利用了JVM类加载机制来保证线程安全,代码非常优雅,既没有加锁的性能消耗,又实现了延迟加载。
- 原理解析:
当外部类
Singleton被加载时,并不会立即加载内部类SingletonHolder。只有当调用getInstance()方法时,才会触发SingletonHolder的类加载。JVM底层保证了类的静态属性初始化时是线程安全的(通过类初始化锁),所以这里无需使用synchronized。
3. 枚举(Enum)- 最安全/《Effective Java》推荐
《Effective Java》作者 Joshua Bloch 极力推荐的方法。它不仅天生线程安全,还能防止反射和反序列化破坏单例。
- 原理解析: 枚举的实例创建是由JVM在类加载时静态初始化的,天生保证线程安全。
- 最大优势: 前面提到的 DCL 和 静态内部类,理论上都可以通过反射(Reflection)强制调用私有构造器,或者通过反序列化(Deserialization)重新生成新对象来破坏单例。而 Java 规范底层从根本上限制了枚举无法被反射创建,且自带安全的序列化机制。
4. 饿汉式(Eager Initialization)- 最简单
如果这个单例对象在程序启动时就一定要使用,且占用内存不大,可以直接使用饿汉式。
- 原理解析: 依赖JVM的类加载机制保证了实例创建时的线程安全。
- 缺点: 没有实现延迟加载(Lazy Loading)。即使程序从头到尾都没用到这个单例,它也会在类加载时被创建,可能会浪费内存。
总结:日常开发该怎么选?
- 单例对象占用资源少,不需要延迟加载: 直接选 饿汉式。
- 对性能有要求,且需要延迟加载: 选 静态内部类 或 双重检查锁(DCL)。
- 涉及序列化/反序列化,或者想要最严密的安全性: 选 枚举(Enum)。

