Java 单例模式讲解
目录
1. 饿汉式(Eager Initialization):“饿了就马上吃”
2. 懒汉式(Lazy Initialization):“需要时才创建”
3. 双重检查锁(Double-Checked Locking):“两次检查,兼顾安全与性能”
4. 静态内部类(Static Inner Class):“懒加载 + 天然线程安全”
单例模式(Singleton Pattern)是 Java 设计模式中创建型模式的一种,核心目标是保证一个类在整个应用中只有一个实例,并提供一个全局唯一的访问点。它的设计思想是 “控制实例数量,避免资源浪费”,广泛应用于全局配置、工具类、线程池等场景。
一、单例模式的核心要素
要实现单例模式,必须满足三个关键条件:
- 私有构造器(Private Constructor):禁止外部通过
new关键字创建实例(切断外部实例化的途径)。 - 静态私有实例(Static Private Instance):在类内部创建唯一实例,并用
private static修饰(确保全局只有一个实例,且属于类级别的资源)。 - 静态公有访问方法(Static Public Access Method):提供一个全局访问点(如
getInstance()),返回内部创建的唯一实例。
二、常见实现方式及优缺点(附代码)
单例模式有多种实现方式,核心区别在于实例创建时机和线程安全处理,不同场景下需选择合适的方式。
1. 饿汉式(Eager Initialization):“饿了就马上吃”
-
原理:类加载时就创建实例(“饿” 表示急切,不延迟)。
-
代码实现:
java
运行
public class EagerSingleton { // 1. 私有构造器:禁止外部实例化 private EagerSingleton() {} // 2. 静态私有实例:类加载时就初始化(唯一实例) private static final EagerSingleton INSTANCE = new EagerSingleton(); // 3. 静态公有方法:提供全局访问点 public static EagerSingleton getInstance() { return INSTANCE; } } -
优点:
- 实现简单,无需考虑线程安全(类加载由 JVM 保证线程安全,同一类只会加载一次)。
-
缺点:
- 不支持延迟加载(类加载时就创建实例),如果实例初始化成本高(如占用大量内存),但程序全程未使用,会造成资源浪费。
-
适用场景:实例初始化成本低,且肯定会被使用(如基础工具类)。
2. 懒汉式(Lazy Initialization):“需要时才创建”
-
原理:第一次调用
getInstance()时才创建实例(“懒” 表示延迟,节省资源)。 -
基础版(线程不安全):
java
运行
public class LazySingleton { private LazySingleton() {} // 不初始化,等第一次调用时再创建 private static LazySingleton instance; public static LazySingleton getInstance() { if (instance == null) { // 问题:多线程下可能同时进入此处,创建多个实例 instance = new LazySingleton(); } return instance; } }问题:多线程并发调用
getInstance()时,可能有多个线程同时判断instance == null,导致创建多个实例(破坏单例)。 -
线程安全版(同步方法):
java
运行
public class LazySingleton { private LazySingleton() {} private static LazySingleton instance; // 加 synchronized 保证线程安全 public static synchronized LazySingleton getInstance() { if (instance == null) { instance = new LazySingleton(); } return instance; } }优点:解决线程安全问题(同步方法保证同一时间只有一个线程执行)。缺点:性能差(每次调用
getInstance()都要加锁,而锁竞争会消耗资源,实际上只有第一次初始化需要锁)。
3. 双重检查锁(Double-Checked Locking):“两次检查,兼顾安全与性能”
-
原理:通过 “两次判断实例是否为空”+“volatile 关键字”,既保证线程安全,又减少锁竞争(只有第一次初始化时加锁)。
-
代码实现:
java
运行
public class DoubleCheckedLockingSingleton { private DoubleCheckedLockingSingleton() {} // volatile 关键:防止指令重排序(核心!) private static volatile DoubleCheckedLockingSingleton instance; public static DoubleCheckedLockingSingleton getInstance() { // 第一次检查:如果实例已存在,直接返回(无需加锁,提升性能) if (instance == null) { synchronized (DoubleCheckedLockingSingleton.class) { // 第二次检查:进入同步块后再判断,防止多线程同时通过第一次检查 if (instance == null) { instance = new DoubleCheckedLockingSingleton(); } } } return instance; } } -
为什么需要 volatile?
instance = new DoubleCheckedLockingSingleton()不是原子操作,会被拆分为 3 步:- 分配内存空间;
- 初始化实例;
- 将
instance指向内存空间。若没有volatile,JVM 可能对步骤 2 和 3 重排序(先执行 3 再执行 2),导致线程 A 未完成初始化时,线程 B 看到instance != null并直接使用,引发空指针异常。volatile可禁止指令重排序,保证安全性。
-
优点:线程安全,且只有第一次初始化时加锁,性能优秀(推荐用于高并发场景)。
4. 静态内部类(Static Inner Class):“懒加载 + 天然线程安全”
-
原理:利用 JVM 类加载机制,通过静态内部类延迟初始化实例(内部类只有在被使用时才会加载)。
-
代码实现:
java
运行
public class StaticInnerClassSingleton { private StaticInnerClassSingleton() {} // 静态内部类:只在被调用时才加载 private static class SingletonHolder { // 内部类中创建实例,由 JVM 保证线程安全(类加载过程是线程安全的) private static final StaticInnerClassSingleton INSTANCE = new StaticInnerClassSingleton(); } // 外部类调用时,才会加载内部类并初始化实例 public static StaticInnerClassSingleton getInstance() { return SingletonHolder.INSTANCE; } } -
优点:
- 懒加载:内部类不被调用就不会加载,节省资源;
- 线程安全:JVM 保证类加载过程的线程安全,无需额外同步;
- 性能好:没有锁竞争,比双重检查锁更简洁。
-
缺点:无法通过反射或反序列化破坏单例(相对安全,但仍需额外处理极端情况)。
-
适用场景:绝大多数单例场景(推荐作为首选实现)。
5. 枚举(Enum):“天然防反射、防序列化”
-
原理:Java 枚举的本质是特殊的类,JVM 保证枚举实例的唯一性,且天然防止反射和反序列化攻击。
-
代码实现:
java
运行
public enum EnumSingleton { // 枚举实例(全局唯一) INSTANCE; // 枚举可以有自己的方法 public void doSomething() { System.out.println("单例方法执行"); } } // 使用:直接通过枚举名.INSTANCE调用 public class Client { public static void main(String[] args) { EnumSingleton.INSTANCE.doSomething(); } } -
优点:
- 绝对线程安全:枚举实例由 JVM 管理,加载时自动初始化,且不可被修改;
- 防反射攻击:反射无法创建枚举实例(
Constructor.newInstance()会抛异常); - 防序列化攻击:枚举的反序列化会直接返回已有的实例,不会创建新对象。
-
缺点:不支持懒加载(枚举类加载时就会初始化实例)。
-
适用场景:对安全性要求极高的场景(如全局配置、加密工具)。
三、单例模式的 “破坏者” 与解决方案
即使实现了上述单例,仍可能被反射或反序列化破坏(枚举除外),需要额外处理。
1. 反射攻击及防御
-
攻击方式:通过反射调用私有构造器,强制创建新实例:
java
运行
// 反射破坏单例(以静态内部类为例) Class<?> clazz = StaticInnerClassSingleton.class; Constructor<?> constructor = clazz.getDeclaredConstructor(); constructor.setAccessible(true); // 绕过私有修饰符 StaticInnerClassSingleton instance1 = (StaticInnerClassSingleton) constructor.newInstance(); StaticInnerClassSingleton instance2 = StaticInnerClassSingleton.getInstance(); System.out.println(instance1 == instance2); // false(单例被破坏) -
防御方案:在私有构造器中添加判断,若实例已存在则抛异常:
java
运行
private StaticInnerClassSingleton() { if (SingletonHolder.INSTANCE != null) { throw new RuntimeException("禁止通过反射创建实例"); } }
2. 反序列化攻击及防御
-
攻击方式:将单例对象序列化后再反序列化,会生成新实例(枚举除外):
java
运行
// 序列化破坏单例(需实现Serializable接口) StaticInnerClassSingleton instance1 = StaticInnerClassSingleton.getInstance(); // 序列化 ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("singleton.txt")); oos.writeObject(instance1); // 反序列化 ObjectInputStream ois = new ObjectInputStream(new FileInputStream("singleton.txt")); StaticInnerClassSingleton instance2 = (StaticInnerClassSingleton) ois.readObject(); System.out.println(instance1 == instance2); // false(单例被破坏) -
防御方案:重写
readResolve()方法,反序列化时返回已有实例:java
运行
public class StaticInnerClassSingleton implements Serializable { // ... 其他代码省略 ... // 反序列化时调用,返回已有实例 private Object readResolve() { return SingletonHolder.INSTANCE; } }
四、单例模式的应用场景
单例模式适用于 “全局只需要一个实例,且实例创建成本高或需要统一管理” 的场景:
- 工具类:如日志工具(
Logger)、日期工具(DateUtils),避免重复创建工具实例浪费资源。 - 全局配置:如
Config类,存储应用的全局配置(数据库连接参数、系统参数等),确保配置一致性。 - 资源管理器:如数据库连接池(
ConnectionPool)、线程池(ThreadPool),控制资源数量,避免资源耗尽。 - 设备管理器:如打印机管理器(
PrinterManager),同一台打印机只能被一个管理器控制。
五、单例模式的优缺点
| 优点 | 缺点 |
|---|---|
| 1. 控制实例数量,节省系统资源;2. 提供全局访问点,方便统一管理;3. 避免资源多重占用(如文件锁冲突)。 | 1. 单例类职责过重,违背 “单一职责原则”(既负责业务逻辑,又负责实例管理);2. 不利于测试(单例依赖全局状态,测试用例可能互相干扰);3. 多线程环境下需额外处理线程安全,实现复杂。 |
总结
单例模式的核心是 “唯一实例 + 全局访问”,不同实现方式各有优劣:
- 简单场景用静态内部类(懒加载 + 线程安全 + 简洁);
- 高并发场景用双重检查锁(兼顾安全与性能);
- 安全性要求极高(防反射 / 反序列化)用枚举;
- 实例初始化成本低且必被使用用饿汉式。
实际开发中需根据场景选择,同时注意防御反射和反序列化攻击,避免单例被破坏。
更多推荐


所有评论(0)