单例模式
- 什么是单例模式?
- 单例模式的实现方式
-
- [1. 饿汉模式 (Eager Initialization)](#1. 饿汉模式 (Eager Initialization))
- [2.懒汉模式(Lazy Initialization)](#2.懒汉模式(Lazy Initialization))
-
- [为什么要加锁?------ 懒汉式单例的线程安全问题⭐](#为什么要加锁?—— 懒汉式单例的线程安全问题⭐)
- [双重 if 的作用------ 懒汉式单例的效率提升⭐](#双重 if 的作用—— 懒汉式单例的效率提升⭐)
- [3. 双重检查锁(DCL)(懒汉模式Plus)](#3. 双重检查锁(DCL)(懒汉模式Plus))
-
- [为什么必须加 `volatile`?------ 懒汉式单例中线程安全问题⭐](#为什么必须加
volatile?—— 懒汉式单例中线程安全问题⭐)
- [为什么必须加 `volatile`?------ 懒汉式单例中线程安全问题⭐](#为什么必须加
- [4. 静态内部类(懒汉模式Pro Max)](#4. 静态内部类(懒汉模式Pro Max))
- 单例模式的缺陷
- 笔试面试题
-
- 写一个单例模式
- 饿汉式和懒汉式有什么区别?
- 懒汉式怎么解决线程安全问题?
- DCL为什么要两次判空?
- [DCL 为什么要加 volatile?](#DCL 为什么要加 volatile?)
- 单例模式可能造成什么问题?
- [静态内部类方式和 DCL 哪个更好?为什么?](#静态内部类方式和 DCL 哪个更好?为什么?)
- 枚举单例为什么被认为是最完美的单例?
- DCL、静态内部类方式和枚举的选择
单例模式(Singleton Pattern)是最简单、最常用的创建型模式之一。
什么是单例模式?
单例模式确保一个类在整个应用程序生命周期中只有一个实例,并提供一个全局访问点。这种设计模式在以下场景特别适用:
- 数据库连接池
- 日志记录器
- 配置管理类
- 线程池
- 缓存管理器
单例模式的实现方式
1. 饿汉模式 (Eager Initialization)
java
class Singleton {
// 1. 静态实例
private static Singleton instance = new Singleton();
// 2. 必须私有化构造方法!否则外部还是可以 new 出来,单例就失效了
private Singleton() {}
// 3. 对外提供接口
public static Singleton getInstance() {
return instance;
}
}
代码中有三个关键点决定了它是单例:
private static Singleton instance = new Singleton();:static关键词意味着这个变量属于类,而不属于某个具体的对象,它在类被加载时就会初始化。类一加载,对象就创建好了。(立即实例化)public static Singleton getInstance(): 这是外部获取该实例的唯一通道。private Singleton() {}:在标准的单例模式中,必须有一个私有构造方法,防止外部通过new Singleton()再次创建对象。(只有一个实例)
Q:为什么叫饿汉模式?
这个名字很形象。它就像一个饿坏了的人,不管你现在需不需要这个对象,他在类加载阶段就把对象"吃"进内存创建好了。
饿汉模式优点
- 线程安全 :这是它最大的优势。Java 虚拟机(JVM)在加载类时,是天然线程安全的。在多线程环境下,绝对不会出现创建出两个
instance的情况。 - 执行效率高 :获取实例时不需要任何
synchronized锁或条件判断,直接返回已经创建好的对象。
饿汉模式缺点
- 资源浪费(内存占用):如果这个单例对象的创建非常耗费资源,而程序从始至终都没用到这个类,那么这个对象依然会常驻内存,白白占用空间。
- 不支持延迟加载:与懒汉模式(需要时再创建)相反。
2.懒汉模式(Lazy Initialization)
java
class SingletonLazy {
// 1. 静态实例
private static SingletonLazy instance = null;
// 2. 私有构造方法,防止外部 new 实例
private SingletonLazy() {}
public static SingletonLazy getInstance() {
// 3. 第一重检查:如果实例已经创建,直接返回,避免不必要的锁竞争
if (instance == null) {
// 4. 加锁,确保只有一个线程能进入创建逻辑
synchronized (SingletonLazy.class) {
// 5. 第二重检查:抢到锁后再次确认实例是否被其他线程先一步创建
if (instance == null) {
instance = new SingletonLazy();
}
}
}
return instance;
}
}
代码中有三个关键点决定了它是单例:
-
private static SingletonLazy instance = null;初始状态下,只是声明了一个引用,并没有创建真正的对象。这节省了程序启动时的内存开销。 -
private SingletonLazy() {}:在标准的单例模式中,必须有一个私有构造方法,防止外部通过new Singleton()再次创建对象。(只有一个实例)
为什么要加锁?------ 懒汉式单例的线程安全问题⭐
没有加锁的代码如下:
java
class SingletonLazy {
// 1. 静态实例
private static SingletonLazy instance = null;
// 2. 私有构造方法,防止外部 new 实例
private SingletonLazy() {}
public static SingletonLazy getInstance() {
if (instance == null) { // ① 第一次检查
instance = new SingletonLazy(); // ② 创建实例
}
return instance;
}
}
在多线程环境下,线程 1和线程 2 可能同时进入if (instance == null)判断,导致 new 两次,破坏了单例原则。

加锁解决方案
方案一:同步方法(简单但性能稍差)
java
public static synchronized SingletonLazy getInstance() {
if (instance == null) {
instance = new SingletonLazy();
}
return instance;
}
synchronized 保证同一时刻只有一个线程能执行该方法,其他线程必须等待。
方案二:双重检查锁(高性能推荐)
java
public static SingletonLazy getInstance() {
if (instance == null) { // 第一次检查(无锁)
synchronized (SingletonLazy.class) {
if (instance == null) { // 第二次检查(有锁)
instance = new SingletonLazy();
}
}
}
return instance;
}
双重 if 的作用------ 懒汉式单例的效率提升⭐
- 外层
if:是为了效率 。避免每次调用getInstance()都加锁,当实例已经创建完成后,后续所有线程直接跳过同步代码块,快速返回实例。 - 内层
if:是为了安全。防止多个线程在外层判断通过后,重复创建实例。
3. 双重检查锁(DCL)(懒汉模式Plus)
双重检查锁(Double-Checked Locking,DCL)
java
class SingletonLazy {
// 1. 使用 volatile 关键字,禁止指令重排序,确保多线程下的可见性
private static volatile SingletonLazy instance = null;
// 2. 私有构造方法,防止外部 new 实例
private SingletonLazy() {}
public static SingletonLazy getInstance() {
// 3. 第一重检查:如果实例已经创建,直接返回,避免不必要的锁竞争
if (instance == null) {
// 4. 加锁,确保只有一个线程能进入创建逻辑
synchronized (SingletonLazy.class) {
// 5. 第二重检查:抢到锁后再次确认实例是否被其他线程先一步创建
if (instance == null) {
instance = new SingletonLazy();
}
}
}
return instance;
}
}
要让单例模式的懒汉模式 (Lazy Initialization)真正达到工业级的标准,代码中必须解决两个核心问题:原子性 和指令重排序。
为什么必须加 volatile?------ 懒汉式单例中线程安全问题⭐
instance = new SingletonLazy(); 这行代码在 CPU 层面其实分为三步:
- 分配内存空间。
- 初始化对象(执行构造函数)。
- 将引用指向内存空间。
如果没有 volatile,编译器可能会为了优化而进行指令重排序 ,变成 1 -> 3 -> 2。

如果执行到 3 时(对象还没初始化完),另一个线程来了,执行了最外层的 if (instance == null)。它会发现 instance 不为 null,于是直接拿走了一个还没初始化完成的对象,导致程序报错。
4. 静态内部类(懒汉模式Pro Max)
利用 JVM 的类加载机制保证线程安全:
java
class Singleton {
private Singleton() {}
// 只有在调用 getInstance() 时,内部类 InnerClass 才会加载
// 加载过程中 JVM 会保证线程安全,同时实现了延迟初始化
private static class InnerClass {
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return InnerClass.INSTANCE;
}
}
这是最优雅的实现方式,既实现了懒加载,又保证了线程安全。
单例模式的缺陷
集群环境------伪单例
在分布式部署中,每个 JVM 实例都有自己的单例,造成伪单例:
java
// 这不是真正的全局单例!
public class UserSessionManager {
// 在集群中,每个节点都有自己的实例
private static UserSessionManager instance;
}
- 使用分布式缓存(如 Redis)
- 使用数据库存储共享状态
- 使用应用服务器的分布式会话管理
序列化------破坏单例
java
public class SerializedSingleton implements Serializable {
private static final long serialVersionUID = 1L;
private static final SerializedSingleton INSTANCE = new SerializedSingleton();
private SerializedSingleton() {}
public static SerializedSingleton getInstance() {
return INSTANCE;
}
// 防止反序列化创建新实例
protected Object readResolve() {
return INSTANCE;
}
}
反射------破坏单例
反射可以调用私有构造器创建新实例:
java
Constructor<Logger> constructor = Logger.class.getDeclaredConstructor();
constructor.setAccessible(true);
Logger newInstance = constructor.newInstance(); // 破坏了单例!
防御措施:
java
public class SecureSingleton {
private static final SecureSingleton INSTANCE = new SecureSingleton();
private static boolean initialized = false;
private SecureSingleton() {
if (initialized) {
throw new RuntimeException("单例模式被破坏");
}
initialized = true;
}
public static SecureSingleton getInstance() {
return INSTANCE;
}
}
笔试面试题
写一个单例模式
java
class Singleton {
// 1. 静态实例
private static Singleton instance = new Singleton();
// 2. 必须私有化构造方法!否则外部还是可以 new 出来,单例就失效了
private Singleton() {}
// 3. 对外提供接口
public static Singleton getInstance() {
return instance;
}
}
java
class SingletonLazy {
// 1. 使用 volatile 关键字,禁止指令重排序,确保多线程下的可见性
private static volatile SingletonLazy instance = null;
// 2. 私有构造方法,防止外部 new 实例
private SingletonLazy() {}
public static SingletonLazy getInstance() {
// 3. 第一重检查:如果实例已经创建,直接返回,避免不必要的锁竞争
if (instance == null) {
// 4. 加锁,确保只有一个线程能进入创建逻辑
synchronized (SingletonLazy.class) {
// 5. 第二重检查:抢到锁后再次确认实例是否被其他线程先一步创建
if (instance == null) {
instance = new SingletonLazy();
}
}
}
return instance;
}
}
饿汉式和懒汉式有什么区别?
| 对比项 | 饿汉式 | 懒汉式 |
|---|---|---|
| 创建时机 | 类加载时 | 首次调用时 |
| 线程安全 | ✅ 天然安全 | ❌ 不安全 |
| 内存占用 | 可能浪费 | 按需分配 |
| 实现复杂度 | 简单 | 需要加锁 |
懒汉式怎么解决线程安全问题?
加
synchronized关键字 +volatile关键字
DCL为什么要两次判空?
java
public static Singleton getInstance() {
if (instance == null) { // 第一次检查:为了效率
synchronized (Singleton.class) {
if (instance == null) { // 第二次检查:为了安全
instance = new Singleton();
}
}
}
return instance;
}
- 外层 if:避免每次调用都加锁。实例创建后,99.9% 的调用直接返回,无需竞争锁
- 内层 if:防止多个线程同时通过外层判断后重复创建。当 T1 创建完实例释放锁,T2 拿到锁后如果不再次判断,就会再 new 一个,破坏单例
DCL 为什么要加 volatile?
instance = new Singleton()不是原子操作,JVM 会分三步:
- 分配内存空间
- 初始化对象
- 将引用指向内存地址
指令重排序可能导致步骤 3 先于步骤 2 执行。此时线程 T1 刚分配内存并赋值引用,实例还未初始化,线程 T2 进来发现
instance != null,直接返回一个未初始化完成的对象,程序崩溃。
volatile通过内存屏障禁止指令重排序,保证对象完全初始化后才赋值引用。
单例模式可能造成什么问题?
内存泄漏陷阱:
java
// ❌ 错误用法------Activity 无法被回收
public class Singleton {
private Context mContext;
private Singleton(Context context) {
this.mContext = context; // 持有 Activity 引用
}
}
// ✅ 正确用法
mContext = context.getApplicationContext(); // 用 ApplicationContext
集群环境问题:分布式部署中,每个 JVM 都有自己的单例,需要通过 Redis 等集中式缓存实现真正的全局单例。
静态内部类方式和 DCL 哪个更好?为什么?
《Java并发编程实战》推荐静态内部类方式优于 DCL。
java
public class Singleton {
private Singleton() {}
private static class Holder {
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return Holder.INSTANCE;
}
}
- 利用 JVM 类加载机制保证线程安全,无需显式同步
- 实现更简洁,没有 volatile 和锁的开销
- 天然支持延迟加载(Holder 类只在第一次调用时加载)
枚举单例为什么被认为是最完美的单例?
- 天然线程安全:JVM 保证枚举实例唯一
- 防反射攻击:反射无法创建枚举实例
- 防序列化破坏:枚举的序列化机制保证返回同一实例,无需额外代码
DCL、静态内部类方式和枚举的选择
DCL 方式的 volatile 虽然解决了指令重排序问题,但有一定性能开销。在实际项目中,如果不需要延迟加载,优先选择静态内部类方式;如果需要防止反射和序列化攻击,枚举是更好的选择。具体选型要看业务场景。