单例模式的6种实现方法,和容器式单例
Java 单例模式完全指南:七种实现方式详解
单例模式(Singleton)保证一个类在 JVM 中只有一个实例,并提供一个全局访问点。
目录
为什么需要单例
典型使用场景:
- 配置管理器:全局一份配置,避免重复读取
- 数据库连接池:昂贵资源只初始化一次
- 日志工具类:统一输出入口
- 线程池 / 缓存:控制实例数量,节省内存
单例的核心约束:
| 要求 | 实现手段 |
|---|---|
| 构造器私有 | private 构造函数,外部无法 new |
| 静态访问点 | getInstance() 或枚举常量 |
| 唯一实例 | 静态变量 / 枚举 / 容器保证 |
1. 懒汉式:双重检查锁定(DCL)
特点:第一次调用 getInstance() 时才创建实例(懒加载),通过双重 if + synchronized 保证线程安全。
完整代码
package com.zx.generater;
/**
* 懒汉式 - 双重检查锁定(DCL)
*/
public class OneGenerator {
// ① 必须用 volatile,防止指令重排导致半初始化对象被读到
private static volatile OneGenerator instance;
private OneGenerator() {
}
public static OneGenerator getInstance() {
// ② 第一次检查:已创建则直接返回,避免每次都加锁
if (instance == null) {
synchronized (OneGenerator.class) {
// ③ 第二次检查:防止多个线程同时通过第一次检查后重复创建
if (instance == null) {
instance = new OneGenerator();
}
}
}
return instance;
}
}
关键细节
为什么需要 volatile?
instance = new OneGenerator() 在字节码层面并非原子操作,大致分为三步:
- 分配内存
- 初始化对象
- 将引用指向内存
JVM 可能对 2、3 步重排序。若线程 A 执行到第 3 步后、第 2 步前被挂起,线程 B 在第一次 if (instance == null) 时可能读到非 null 但未完全初始化的对象。
volatile 禁止这种重排序,并保证多线程间的可见性。
为什么要检查两次?
- 第一次检查(锁外):实例已存在时,避免每次调用都进入
synchronized,性能更好。 - 第二次检查(锁内):多个线程同时通过第一次检查时,只有一个能创建实例。
优缺点
| 优点 | 缺点 |
|---|---|
| 懒加载,节省资源 | 代码较复杂 |
| 线程安全 | 仍可能被反射破坏(构造器未防护) |
| 高并发下性能较好 | 仍可能被序列化破坏(未实现 readResolve) |
2. 饿汉式:类加载时创建
特点:类被加载时即创建实例,天然线程安全,实现最简单。
完整代码
package com.zx.generater;
/**
* 饿汉式 - 类加载时创建实例
*/
public class TwoGenerate {
// 类加载阶段由类加载器初始化,JVM 保证线程安全
private static final TwoGenerate INSTANCE = new TwoGenerate();
private TwoGenerate() {
}
public static TwoGenerate getInstance() {
return INSTANCE;
}
}
关键细节
类加载时机
static 字段在类初始化阶段(<clinit>)赋值。JVM 规范保证:同一个类的 <clinit> 在同一 ClassLoader 下只执行一次,且由 JVM 加锁,因此无需 synchronized。
是否算「懒加载」?
不算。只要类被引用(哪怕只调用一个静态方法),就会触发类加载,实例随即创建。若实例很重且不一定用到,会造成浪费。
优缺点
| 优点 | 缺点 |
|---|---|
| 实现极简 | 非懒加载,可能浪费内存 |
| 天然线程安全 | 无法传参初始化 |
| 无锁,性能好 | 同样可被反射/序列化破坏 |
3. 静态内部类(Holder)
特点:结合懒加载与线程安全,被广泛认为是最优雅的普通类单例写法。
完整代码
package com.zx.generater;
/**
* 静态内部类(Holder)- 懒加载且线程安全
*/
public class ThreeGenerate {
private ThreeGenerate() {
}
// 内部类持有唯一实例
private static class Holder {
private static final ThreeGenerate INSTANCE = new ThreeGenerate();
}
public static ThreeGenerate getInstance() {
// 首次调用时才加载 Holder 类,进而创建 INSTANCE
return Holder.INSTANCE;
}
}
关键细节
加载顺序
- 外部类
ThreeGenerate被加载时,不会立即加载Holder。 - 第一次调用
getInstance()时,才触发Holder的类加载。 Holder初始化时创建INSTANCE,由 JVM 保证线程安全。
这实现了按需懒加载,又避免了 DCL 的 volatile 和双重 if。
与饿汉式的区别
饿汉式在外部类加载时就创建实例;Holder 把实例创建推迟到内部类首次被访问时。
优缺点
| 优点 | 缺点 |
|---|---|
| 懒加载 + 线程安全 | 写法略绕(需理解类加载) |
| 无需 synchronized / volatile | 可被反射/序列化破坏 |
| 代码简洁、性能好 | — |
4. 枚举单例
特点:Joshua Bloch 在《Effective Java》中推荐的方式,最简洁且最安全。
完整代码
package com.zx.generater;
/**
* 枚举单例 - 天然防反射、防序列化破坏
*/
public enum FourGenerate {
INSTANCE;
public void doSomething() {
// 业务方法示例
}
}
使用方式
FourGenerate.INSTANCE.doSomething();
关键细节
为什么能防反射?
反射调用 Constructor.newInstance() 时,若目标是枚举,会抛出:
java.lang.IllegalArgumentException: Cannot reflectively create enum objects
JVM 在反射层面禁止实例化枚举。
为什么能防序列化?
普通对象反序列化会调用构造器创建新对象。枚举的反序列化机制不同:反序列化时不会调用构造器,而是根据枚举名查找已有常量,因此始终是同一个 INSTANCE。
是否可以懒加载?
枚举实例在枚举类加载时创建,属于饿汉式,无法做到 Holder 那种延迟。
优缺点
| 优点 | 缺点 |
|---|---|
| 代码极简 | 非懒加载 |
| 防反射、防序列化 | 无法继承(枚举隐式继承 Enum) |
| 序列化安全 | 语义上「枚举即单例」需团队接受 |
5. 防反射 + 防序列化破坏
特点:在 DCL 基础上,针对反射和序列化两种「破坏单例」的手段做防护。
完整代码
package com.zx.generater;
import java.io.Serializable;
/**
* 防反射 + 防序列化破坏的单例
*/
public class FiveGenerate implements Serializable {
private static volatile FiveGenerate instance;
private FiveGenerate() {
// 防反射:若 instance 已存在,说明是通过 getInstance 创建的合法实例
// 反射第二次调用构造器时 instance 非 null,直接抛异常
if (instance != null) {
throw new IllegalStateException("禁止通过反射创建单例");
}
}
public static FiveGenerate getInstance() {
if (instance == null) {
synchronized (FiveGenerate.class) {
if (instance == null) {
instance = new FiveGenerate();
}
}
}
return instance;
}
/**
* 防序列化:反序列化时 JVM 会调用 readResolve 替代默认新建对象的行为
* 始终返回已存在的单例
*/
private Object readResolve() {
return getInstance();
}
}
关键细节
反射攻击原理
Constructor<FiveGenerate> ctor = FiveGenerate.class.getDeclaredConstructor();
ctor.setAccessible(true);
FiveGenerate fake = ctor.newInstance(); // 若不防护,会创建第二个实例
防护逻辑:
- 正常流程:
getInstance()内instance = new FiveGenerate(),此时构造器里instance == null,通过检查。 - 反射流程:再次
new FiveGenerate()时,instance已非 null,构造器抛异常。
注意:若攻击者先用反射创建实例(在
getInstance之前),仍可能破坏单例。更严谨的做法是在静态块中设置标志位,或使用枚举单例。
序列化攻击原理
FiveGenerate a = FiveGenerate.getInstance();
// 序列化再反序列化
FiveGenerate b = (FiveGenerate) deserialize(serialize(a));
// 若无 readResolve,a != b
readResolve() 让反序列化返回已有单例,而不是新建对象。
优缺点
| 优点 | 缺点 |
|---|---|
| 懒加载 + 线程安全 | 代码最复杂 |
| 防反射(常规路径) | 反射防护仍有边界情况 |
| 防序列化 | 维护成本高 |
6. ThreadLocal 单例
特点:每个线程各自持有一个实例,是「线程内单例」,不是全局单例。
完整代码
package com.zx.generater;
/**
* ThreadLocal 单例 - 每个线程各自持有一个实例
*/
public class SixGenerate {
private static final ThreadLocal<SixGenerate> INSTANCE =
ThreadLocal.withInitial(SixGenerate::new);
private SixGenerate() {
}
public static SixGenerate getInstance() {
return INSTANCE.get();
}
/**
* 线程池场景下必须调用,否则可能内存泄漏
*/
public static void remove() {
INSTANCE.remove();
}
}
关键细节
与全局单例的区别
| 维度 | 全局单例 | ThreadLocal 单例 |
|---|---|---|
| 实例数量 | 整个 JVM 一个 | 每个线程一个 |
| 线程安全 | 实例本身需考虑并发 | 天然线程隔离 |
| 典型场景 | 配置、连接池 | SimpleDateFormat、用户上下文 |
withInitial(SixGenerate::new)
线程第一次调用 get() 时,若当前线程没有绑定值,会执行 SixGenerate::new 创建实例并缓存到当前线程的 ThreadLocalMap 中。
为什么需要 remove()?
线程池中的工作线程会被复用。若不在任务结束后 remove(),ThreadLocal 中的对象无法被 GC,造成内存泄漏(线程不死,Map 条目一直在)。
try {
SixGenerate ctx = SixGenerate.getInstance();
// 使用 ctx ...
} finally {
SixGenerate.remove(); // 线程池场景务必清理
}
优缺点
| 优点 | 缺点 |
|---|---|
| 无锁,线程内高性能 | 不是真正的全局单例 |
| 避免共享状态竞争 | 线程池下需手动 remove |
| 适合「每线程一份」的资源 | 实例总数 = 线程数,占内存 |
7. 容器式单例
特点:用一个注册表按 key 管理多组单例,适合「多种类型、各保唯一」的场景。
完整代码
package com.zx.generater;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Supplier;
/**
* 容器式单例 - 按 key 管理多个唯一实例
*/
public final class SingletonManager {
private static final Map<String, Object> INSTANCES = new ConcurrentHashMap<>();
private SingletonManager() {
}
@SuppressWarnings("unchecked")
public static <T> T getInstance(String key, Supplier<T> creator) {
return (T) INSTANCES.computeIfAbsent(key, k -> creator.get());
}
}
使用示例
// 同一 key 始终返回同一实例
ConnectionPool pool = SingletonManager.getInstance("db-pool", ConnectionPool::new);
CacheManager cache = SingletonManager.getInstance("cache", () -> new CacheManager(1024));
// 不同 key 各自唯一
ConnectionPool pool2 = SingletonManager.getInstance("redis-pool", ConnectionPool::new);
关键细节
computeIfAbsent 的原子性
ConcurrentHashMap.computeIfAbsent 对同一 key 的 compute 是原子的:多个线程同时为同一 key 调用时,只有一个 creator.get() 会执行,其余线程拿到已放入 Map 的实例。
Supplier<T> creator 的作用
- 延迟创建:只有第一次请求该 key 时才执行
creator。 - 灵活传参:可用 lambda 捕获构造参数。
与 Spring 容器的关系
思想类似「IoC 容器按 Bean 名称管理单例」,但这里是轻量级手写版,不依赖框架。
优缺点
| 优点 | 缺点 |
|---|---|
| 一个管理器管多种单例 | 需约定 key 命名,避免冲突 |
| 懒加载 + 线程安全 | 类型安全依赖泛型强转 |
| 扩展方便 | 生命周期管理需自行处理(无销毁钩子) |
七种方式对比
| 实现 | 懒加载 | 线程安全 | 防反射 | 防序列化 | 复杂度 | 推荐度 |
|---|---|---|---|---|---|---|
| DCL 懒汉式 | ✅ | ✅ | ❌ | ❌ | 中 | ⭐⭐⭐ |
| 饿汉式 | ❌ | ✅ | ❌ | ❌ | 低 | ⭐⭐⭐ |
| 静态内部类 | ✅ | ✅ | ❌ | ❌ | 低 | ⭐⭐⭐⭐⭐ |
| 枚举单例 | ❌ | ✅ | ✅ | ✅ | 极低 | ⭐⭐⭐⭐⭐ |
| 防反射/序列化 | ✅ | ✅ | ✅* | ✅ | 高 | ⭐⭐⭐ |
| ThreadLocal | 按线程 | ✅ | ❌ | ❌ | 中 | 场景限定 |
| 容器式 | ✅ | ✅ | ❌ | ❌ | 中 | 多实例管理 |
* 防反射在常规攻击路径下有效,极端顺序下仍有漏洞。
选型建议
需要最简单、最安全的单例?
└─> 枚举单例(FourGenerate)
需要懒加载 + 普通类写法?
└─> 静态内部类(ThreeGenerate)
实例必须在类加载时就绪?
└─> 饿汉式(TwoGenerate)
必须懒加载且熟悉 DCL?
└─> 双重检查锁定(OneGenerator)
需要抵御反射和序列化?
└─> 优先枚举;否则 FiveGenerate + 文档说明边界
每个线程独立一份状态?
└─> ThreadLocal(SixGenerate),记得 remove()
多种单例统一注册管理?
└─> SingletonManager 容器式
现代 Java 项目的务实建议
- 默认首选枚举单例——代码少、JDK 背书、无额外坑。
- 需要继承或更多控制时用静态内部类——Holder 模式是业界标准写法。
- 避免手写 DCL——除非有明确理由;容易写错
volatile。 - ThreadLocal 只用于「线程隔离」——不要误当全局单例。
- 单例是否仍必要?——在 Spring 等框架中,
@Component+ 默认单例作用域往往已足够;手写单例更适合工具库、非 Spring 环境或明确要脱离容器控制的场景。
附录:反射破坏单例演示
public class SingletonBreakDemo {
public static void main(String[] args) throws Exception {
OneGenerator a = OneGenerator.getInstance();
Constructor<OneGenerator> ctor = OneGenerator.class.getDeclaredConstructor();
ctor.setAccessible(true);
OneGenerator b = ctor.newInstance();
System.out.println(a == b); // false — 单例被破坏
}
}
枚举单例在同样操作下会直接抛异常,这也是推荐枚举的原因之一。
更多推荐
所有评论(0)