设计模式-单例模式
目录

问题思考
[!blue]
- 说一说常用的设计模式,如何实现一个线程安全的单例模式。
- 单例模式有哪几种实现方式,饿汉式单例模式和懒汉式单例模式的区别以及代码实现。
- 双重校验单例模式为什么需要加两层校验,为什么要使用 volatile 关键字,不使用 volatile 关键字会导致线程不安全吗。
- 使用了 synchronized 为什么还需要使用 volatile,使用 synchronized 的作用是什么。
单例模式
[!blue]
定义:保证一个类仅有一个实例,并提供一个访问它的全局访问点。特点:
- 单例模式只能有一个实例。
- 单例类必须创建自己的唯一实例。
- 单例类必须向其他对象提供这一实例。
单例模式的分类
[!blue]
饿汉式单例模式:立即加载,类一加载就创建实例,天然线程安全,但不能延迟加载。懒汉式单例模式:延迟加载,第一次使用时才创建实例,必须自己解决多线程下的并发创建问题。
面试主线:从线程不安全到三种推荐写法
最朴素的懒汉式是这样写的:
if (instance == null) { instance = new Singleton(); }。它为什么线程不安全?因为「判空」和「创建」不是一个原子操作:线程 A 和线程 B 可能同时通过判空(都看到instance == null),然后各自创建一个实例——单例被破坏。这就是所有线程安全写法要解决的第一个问题。各写法的取舍:同步方法 / 同步代码块直接给整个「判空 + 创建」加锁,正确但锁粒度太大,实例创建完成后每次读取仍要抢锁,性能差;饿汉式(静态常量 / 静态代码块)依靠 JVM 类初始化的加锁机制天然线程安全、零同步开销,但不能延迟加载;双重校验在锁外再加一层判空,兼顾正确性与性能,代价是必须配合 volatile 且写法最复杂。
最终演进出三种推荐写法:双重校验(面试必考、可懒加载)、静态内部类(懒加载 + 零同步开销 + 写法简单)、枚举(《Effective Java》推荐,唯一天然防反射与反序列化的写法)。
如何实现一个线程安全的单例模式
《Effective Java》第 3 条:用私有构造器或者枚举类型强化 Singleton 属性。
一个严谨的单例模式除了保证线程安全外,还需要防止反射、反序列化和 clone 破坏单例。下面是加固后的双重校验完整实现。
import java.io.Serializable;
public class Singleton implements Serializable, Cloneable {
// volatile 禁止指令重排序,并保证写入对其他线程可见
private static volatile Singleton instance = null;
// 私有化构造方法,并防止反射调用
private Singleton() {
if (instance != null) {
throw new RuntimeException("单例模式不允许重复创建对象");
}
}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
// 防止 clone 破坏单例
@Override
protected Object clone() throws CloneNotSupportedException {
return instance;
}
// 防止反序列化破坏单例
private Object readResolve() {
return instance;
}
}
[!red]
- 防反序列化:在类中添加
readResolve()方法并返回已有实例,反序列化时会用该方法的返回值替换新创建的对象。- 防 clone:实现
Cloneable接口并覆盖Object的clone()方法,直接返回已有实例,不产生副本。- 防反射:在构造方法中判断
instance != null就抛异常,让反射在第二次创建实例时失败。注意这只能防「先正常创建、再反射创建」的场景,若反射先执行则防不住——彻底的方案只有枚举。
单例模式的七种实现方式(线程安全)
[!red]
以下七种都是线程安全的实现,按面试主线组织:先看最重要的双重校验(一),再看它的推荐替代静态内部类(二),然后回头看它由之演进而来的粗粒度锁写法(三、四),接着是不能延迟加载的两种饿汉式(五、六),最后是最推荐的枚举(七)。
一、懒汉式-双重校验单例模式

上图即双重校验(Double-Checked Locking)的标准实现,文字版代码如下:两层
if (instance == null)夹着一个synchronized块,实例字段必须用 volatile 修饰。
public class Singleton {
private static volatile Singleton instance = null;
private Singleton() {
}
public static Singleton getInstance() {
if (instance == null) { // 第一次判空:锁外,避免每次调用都抢锁
synchronized (Singleton.class) {
if (instance == null) { // 第二次判空:锁内,保证只创建一次
instance = new Singleton();
}
}
}
return instance;
}
}
为什么要校验两次?第一次判空在锁外,为了性能:实例创建完成后,后续所有调用直接返回,不必再抢锁;第二次判空在锁内,为了正确性:多个线程可能同时通过第一次判空并排队进入同步块,第一个线程创建完实例释放锁后,若后续线程不再检查一次,就会重复创建实例。synchronized 保证了创建过程的互斥与原子性。
为什么还要 volatile?(必考)
instance = new Singleton()不是原子操作,字节码层面分三步:① 分配内存;② 调用构造方法初始化对象;③ 把 instance 引用指向这块内存。②③ 之间没有数据依赖,编译器 / CPU 可能重排序为 ① → ③ → ②。若线程 A 刚执行完 ①③(引用已非空、对象尚未初始化),线程 B 恰好在锁外做第一次判空,会看到instance != null而直接返回这个半初始化对象,使用其字段时就会读到默认值甚至出错。volatile 写之前的操作不能被重排到写之后,即禁止 ②③ 重排,同时保证写入对其他线程立即可见。有了 synchronized 为什么还不够?synchronized 只保证临界区内的互斥,以及进出临界区时的可见性;而出问题的第一次判空是锁外读,完全不受 synchronized 保护,所以必须由 volatile 兜底。两者分工明确:synchronized 管「只创建一次」,volatile 管「锁外读不到半成品」,缺一不可。
二、懒汉式-静态内部类单例模式
public class Singleton {
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
private Singleton() {
}
public static Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
为什么行:外部类 Singleton 被加载时并不会初始化 SingletonHolder,只有第一次调用
getInstance()触发内部类初始化时才创建实例——这就是延迟加载;而类的初始化由 JVM 在内部加锁、保证只执行一次,天然线程安全。既有懒汉式的延迟加载,又没有任何同步开销,也不需要 volatile,写法比双重校验简单得多,是推荐的实现方式之一。
三、懒汉式-同步代码块单例模式
public class Singleton {
private static Singleton instance = null;
private Singleton() {
}
public static Singleton getInstance() {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
}
为什么不推荐:线程安全没问题——判空和创建整个被锁住;但无论实例是否已经创建,每次调用
getInstance()都要先获取锁,锁粒度大、并发性能差。在锁外再加一层判空,就演进成了双重校验。
四、懒汉式-同步方法单例模式
public class Singleton {
private static Singleton instance = null;
private Singleton() {
}
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
为什么不推荐:与同步代码块等价——synchronized 加在静态方法上,锁的同样是
Singleton.class。写法最简单,但同样每次调用都要加锁,性能与写法三相同,不推荐。
五、饿汉式-静态代码块单例模式
public class Singleton {
private static Singleton instance = null;
static {
instance = new Singleton();
}
private Singleton() {
}
public static Singleton getInstance() {
return instance;
}
}
为什么行 / 局限:类的初始化阶段由 JVM 加锁执行且只执行一次,静态代码块里的赋值天然线程安全,还可以在代码块中写额外的初始化逻辑。缺点是类一加载就创建实例,不能延迟加载——如果实例很重且可能整个生命周期都用不到,就白白占用资源。
六、饿汉式-静态常量单例模式
public class Singleton {
private static final Singleton SINGLETON = new Singleton();
private Singleton() {
}
public static Singleton getSingleton() {
return SINGLETON;
}
}
为什么行 / 局限:与静态代码块写法等价,是最简单直接的线程安全实现,同样不能延迟加载。另外注意:前六种写法如果不做额外防护,都可能被反射和反序列化破坏单例,加固方式见上文完整实现。
七、枚举类单例模式
public enum Singleton {
INSTANCE
}
class Test {
public static void main(String[] args) {
Singleton instance1 = Singleton.INSTANCE;
Singleton instance2 = Singleton.INSTANCE;
System.out.println(instance1 == instance2);
}
}
:::fold 运行结果
[!green]
true
:::
为什么最推荐:枚举是《Effective Java》推荐的最佳单例写法,它绝对防止多次实例化,原因有三:
- 线程安全:枚举实例由 JVM 在类初始化时创建,和饿汉式一样天然线程安全,无需任何同步代码。
- 防反射:
Constructor.newInstance()检测到枚举类型会直接抛出IllegalArgumentException,从 JDK 层面禁止反射创建枚举实例——这是前六种写法靠构造方法判断都做不到的「绝对防护」。- 防反序列化:枚举的序列化只写出实例名字,反序列化时通过
valueOf()返回已有实例,无需自己实现readResolve()。虽然这种方法还没有广泛采用,但是单元素的枚举类型经常成为实现 Singleton 的最佳方法。
注意:枚举已隐式继承
java.lang.Enum,如果 Singleton 必须扩展一个其他超类则不宜使用这个方法(虽然可以声明枚举去实现接口);另外枚举本质是饿汉式,不能延迟加载。
单例模式在 JDK 中的应用
[!blue]
- java.lang.Runtime#getRuntime()
- java.awt.Desktop#getDesktop()
- java.lang.System#getSecurityManager()