单例模式从入门到精通:一个数据库连接池的完整剖析
本文通过一个真实的数据库连接池场景,从理论到实践完整讲解单例模式,包含 6 种实现方式对比、线程安全分析、反射攻击与防御、单例反模式批判性分析、Spring 单例 vs GoF 单例辨析、面试高频问题及企业级最佳实践。
目录
- 写在前面:为什么需要单例模式?
- 单例模式的 6 种实现方式
- 数据库连接池完整代码实战
- 单例模式的原理深度剖析
- 单例模式的缺陷与防御(反射 / 序列化 / 克隆)
- 单例模式是反模式吗?------ 工程视角的批判性分析
- 单例模式在框架中的应用(含 Spring 单例 vs GoF 单例辨析)
- JDK 中的真实案例深度剖析
- 六种实现方式完整对比
- 企业级最佳实践
- 面试高频问题及深度解答
- 现代 Java 视角:record / sealed / 模块系统
- 总结与决策流程
一、写在前面:为什么需要单例模式?
1.1 一个真实的业务痛点
在开发企业级应用时,我们经常遇到这样的场景:
java
// ❌ 问题代码:每次创建新的连接池
public class Application {
public void doBusiness() {
// 每次请求都创建新的连接池,资源浪费严重!
DatabaseConnectionPool pool = new DatabaseConnectionPool();
pool.configure("jdbc:mysql://localhost:3306/test", "root", "123456", 10);
Connection conn = pool.getConnection();
// 业务操作...
}
}
这段代码的问题:
- 资源浪费:每次请求都创建新的连接池,内存和 CPU 开销巨大
- 不一致性:每个连接池独立管理连接,无法统一控制
- 性能低下:频繁创建和销毁对象,GC 压力大
解决方案:使用单例模式确保全局只有一个连接池实例!
1.2 单例模式的定义
单例模式(Singleton Pattern) 确保一个类只有一个实例,并提供一个全局访问点。
------ GoF《设计模式:可复用面向对象软件的基础》
核心要点:
- 私有化构造方法,防止外部
new - 提供一个静态方法获取唯一实例
- 确保线程安全(多线程环境下)
二、单例模式的 6 种实现方式
2.1 饿汉式(Eager Initialization)
java
// 文件: EagerSingleton.java
package com.devkit.patterns.creational.singleton;
/**
* 饿汉式单例
* 优点:线程安全,实现简单
* 缺点:类加载即创建,可能浪费内存
*/
class EagerSingleton {
// 类加载时就创建实例
private static final EagerSingleton INSTANCE = new EagerSingleton();
private EagerSingleton() {}
public static EagerSingleton getInstance() {
return INSTANCE;
}
}
| 维度 | 说明 |
|---|---|
| 线程安全 | ✅ 类加载由 JVM 保证线程安全 |
| 延迟加载 | ❌ 类加载即创建 |
| 性能 | ⭐⭐⭐ 无锁无同步 |
| 适用场景 | 实例占用内存小,一定会被使用 |
2.2 懒汉式(线程不安全)
java
// 文件: LazySingletonUnsafe.java
package com.devkit.patterns.creational.singleton;
/**
* 懒汉式单例(线程不安全)
* 优点:延迟加载
* 缺点:多线程环境下会创建多个实例
*/
class LazySingletonUnsafe {
private static LazySingletonUnsafe instance;
private LazySingletonUnsafe() {}
public static LazySingletonUnsafe getInstance() {
// ❌ 线程不安全:两个线程可能同时进入 if
if (instance == null) {
instance = new LazySingletonUnsafe();
}
return instance;
}
}
问题演示:
text
线程A: if (instance == null) → true → 暂停
线程B: if (instance == null) → true → 创建实例
线程A: 继续执行 → 创建另一个实例 → 单例失效!
⚠️ 实际项目中绝对不要用这种写法。 仅作为理解"为什么需要线程安全"的反面教材。
2.3 懒汉式(同步方法)
java
// 文件: LazySingletonSync.java
package com.devkit.patterns.creational.singleton;
/**
* 懒汉式单例(同步方法)
* 优点:线程安全,延迟加载
* 缺点:每次调用都同步,性能差
*/
class LazySingletonSync {
private static LazySingletonSync instance;
private LazySingletonSync() {}
// 整个方法加 synchronized
public static synchronized LazySingletonSync getInstance() {
if (instance == null) {
instance = new LazySingletonSync();
}
return instance;
}
}
性能问题 :每次调用 getInstance() 都要获取锁,即使实例已创建。在高并发场景下,所有线程串行获取实例,性能极差。
2.4 双重检查锁(DCL)⭐
java
// 文件: DatabaseConnectionPool.java
package com.devkit.patterns.creational.singleton;
/**
* 双重检查锁单例
* 优点:线程安全 + 延迟加载 + 高性能
* 关键:volatile + 双重检查
*/
class DatabaseConnectionPool {
// ⭐ volatile 禁止指令重排序,保证可见性
private static volatile DatabaseConnectionPool instance;
private DatabaseConnectionPool() {}
public static DatabaseConnectionPool getInstance() {
// 第一次检查:避免不必要的同步
if (instance == null) {
// 同步代码块:只锁第一次创建
synchronized (DatabaseConnectionPool.class) {
// 第二次检查:防止重复创建
if (instance == null) {
instance = new DatabaseConnectionPool();
}
}
}
return instance;
}
}
核心机制详解:为什么需要 volatile?
java
instance = new DatabaseConnectionPool();
// 这行代码在 JVM 中实际分三步执行:
// 1. memory = allocate() // 分配内存空间
// 2. ctorInstance(memory) // 初始化对象(调用构造方法)
// 3. instance = memory // 将 instance 指向内存地址
// ❌ 如果没有 volatile,JVM 可能将指令重排序为:
// 1. memory = allocate() // 分配内存空间
// 2. instance = memory // 将 instance 指向内存地址(但对象尚未初始化!)
// 3. ctorInstance(memory) // 初始化对象
// 后果:其他线程在第一次检查时看到 instance != null(实际上未初始化完成),
// 直接返回了一个"半成品"对象,导致 NPE 或数据错乱!
💡 volatile 的两个作用
- 禁止指令重排序:确保"分配→初始化→赋值"三步按序执行
- 保证可见性:一个线程修改 instance 后,其他线程立即看到最新值(volatile 写会刷新到主内存,volatile 读会从主内存重新加载)
DCL 的 3 个关键点
| 关键点 | 作用 | 不实现的后果 |
|---|---|---|
| volatile | 禁止指令重排序,保证可见性 | 可能获取未初始化完成的对象 |
| 第一次检查 | 避免每次调用都同步,提升性能 | 每次都要获取锁,性能差 |
| 第二次检查 | 防止多个线程同时创建实例 | 可能创建多个实例 |
2.5 静态内部类(最优雅)⭐
java
// 文件: ConnectionPoolHolder.java
package com.devkit.patterns.creational.singleton;
/**
* 静态内部类单例
* 优点:线程安全 + 延迟加载 + 实现简洁
* 原理:JVM 类加载机制保证线程安全
*/
class ConnectionPoolHolder {
private ConnectionPoolHolder() {}
// 静态内部类:只有在调用 getInstance() 时才会加载
private static class Holder {
private static final ConnectionPoolHolder INSTANCE = new ConnectionPoolHolder();
}
public static ConnectionPoolHolder getInstance() {
return Holder.INSTANCE;
}
}
为什么线程安全?
JVM 在类加载阶段会执行 <clinit>() 方法(类初始化方法),由 JVM 内部保证线程安全 ------同一时间只有一个线程能执行某个类的 <clinit>()。
- 外部类
ConnectionPoolHolder被加载时,不会 加载内部类Holder - 调用
getInstance()→ 引用Holder.INSTANCE→ 触发Holder类加载 - JVM 加载
Holder类 → 执行静态初始化 → 创建INSTANCE(此过程线程安全) - 后续调用直接返回已创建好的
INSTANCE
2.6 枚举单例(最安全)
java
// 文件: DatabaseConnectionPoolEnum.java
package com.devkit.patterns.creational.singleton;
/**
* 枚举单例
* 优点:线程安全 + 天然防止反射攻击 + 天然防止序列化破坏
*/
public enum DatabaseConnectionPoolEnum {
INSTANCE;
private String url;
private int activeConnections;
// 枚举的构造方法默认且必须是 private
DatabaseConnectionPoolEnum() {}
public void configure(String url, int maxPoolSize) {
this.url = url;
this.activeConnections = 0;
System.out.println("枚举单例配置完成: " + url);
}
public Connection getConnection() {
return new Connection(++activeConnections, url, "enumUser");
}
}
// 使用
// DatabaseConnectionPoolEnum.INSTANCE.configure("jdbc:mysql://localhost:3306/test", 10);
// Connection conn = DatabaseConnectionPoolEnum.INSTANCE.getConnection();
为什么最安全?------ 枚举防反射的正确原理
枚举防反射的核心原因不是"没有无参构造",而是 JVM 在 Constructor.newInstance() 源码层面显式拦截了枚举类型:
java
// JDK Constructor.newInstance() 源码(简化)
public T newInstance(Object... initargs) {
// ...
if ((clazz.getModifiers() & Modifier.ENUM) != 0) {
throw new IllegalArgumentException(
"Cannot reflectively create enum objects");
}
// ...
}
// 验证:即使你拿到枚举的正确构造方法,也无法通过反射创建
Constructor<DatabaseConnectionPoolEnum> constructor =
DatabaseConnectionPoolEnum.class.getDeclaredConstructor(
String.class, int.class);
constructor.setAccessible(true);
constructor.newInstance("FAKE", 0);
// → 抛出: IllegalArgumentException: Cannot reflectively create enum objects
枚举防序列化的原理同样明确:Java 序列化规范规定,枚举类型的反序列化通过 Enum.valueOf(name) 实现,直接返回已存在的枚举常量,而非创建新对象。这写在 ObjectInputStream.readEnum() 方法中。
枚举单例是延迟加载吗?
枚举类本身是懒加载的:JVM 在首次使用该枚举类时才加载它。一旦加载,所有枚举常量立即初始化。与饿汉式"类加载即创建"不同,枚举类的加载时机由首次使用决定,粒度是类级别,而非"绝对不延迟"。
准确表述:枚举单例支持类级延迟加载,但不支持方法级延迟加载。
三、数据库连接池完整代码实战
3.1 项目结构
text
src/
├── main/
│ ├── java/
│ │ └── com/devkit/patterns/creational/singleton/
│ │ ├── Connection.java // 连接对象
│ │ ├── DatabaseConnectionPool.java // DCL 单例
│ │ ├── ConnectionPoolHolder.java // 静态内部类单例
│ │ ├── DatabaseConnectionPoolEnum.java // 枚举单例
│ │ ├── ConnectionPoolManager.java // 最佳实践完整版
│ │ └── SingletonDemo.java // 测试入口
│ └── resources/
│ └── application.properties
3.2 Connection 类
java
// ===== 文件: Connection.java =====
package com.devkit.patterns.creational.singleton;
/**
* 数据库连接对象(简化版,仅用于演示单例模式)
* 实际项目中的 Connection 由 JDBC 驱动或连接池(HikariCP 等)提供
*/
public class Connection {
private final int id;
private final String url;
private final String user;
public Connection(int id, String url, String user) {
this.id = id;
this.url = url;
this.user = user;
}
public int getId() {
return id;
}
public String getUrl() {
return url;
}
public String getUser() {
return user;
}
@Override
public String toString() {
return "Connection{id=" + id + ", url='" + url + "', user='" + user + "'}";
}
}
3.3 DCL 单例完整实现
java
// ===== 文件: DatabaseConnectionPool.java =====
package com.devkit.patterns.creational.singleton;
/**
* 数据库连接池 - DCL 单例实现
*
* 业务场景: 全局唯一的连接池管理器,避免每次请求创建新连接池
* 线程安全: volatile + 双重检查锁
*/
public class DatabaseConnectionPool {
// ⭐ volatile 禁止指令重排序
private static volatile DatabaseConnectionPool instance;
private String url;
private String username;
private String password;
private int maxPoolSize;
private int activeConnections = 0;
private DatabaseConnectionPool() {}
public static DatabaseConnectionPool getInstance() {
if (instance == null) {
synchronized (DatabaseConnectionPool.class) {
if (instance == null) {
instance = new DatabaseConnectionPool();
}
}
}
return instance;
}
public void configure(String url, String username, String password, int maxPoolSize) {
this.url = url;
this.username = username;
this.password = password;
this.maxPoolSize = maxPoolSize;
this.activeConnections = 0;
System.out.println("连接池配置完成: " + url);
}
public Connection getConnection() {
if (activeConnections >= maxPoolSize) {
throw new RuntimeException(
"连接池已满,活跃连接: " + activeConnections + "/" + maxPoolSize);
}
activeConnections++;
return new Connection(activeConnections, url, username);
}
public void returnConnection(Connection conn) {
if (activeConnections > 0) {
activeConnections--;
}
System.out.println("归还连接: " + conn);
}
public void printStatus() {
System.out.println("连接池状态: 活跃=" + activeConnections + "/" + maxPoolSize
+ " url=" + url);
}
}
3.4 测试入口
java
// ===== 文件: SingletonDemo.java =====
package com.devkit.patterns.creational.singleton;
/**
* 单例模式演示入口
*/
public class SingletonDemo {
public static void main(String[] args) {
System.out.println("===== DCL 单例演示 =====");
// 1. 获取单例实例
DatabaseConnectionPool pool1 = DatabaseConnectionPool.getInstance();
DatabaseConnectionPool pool2 = DatabaseConnectionPool.getInstance();
// 2. 验证单例
System.out.println("pool1 == pool2: " + (pool1 == pool2)); // true
// 3. 配置连接池
pool1.configure("jdbc:mysql://localhost:3306/orders", "root", "123456", 10);
pool1.printStatus();
// 4. 获取连接
Connection conn = pool1.getConnection();
System.out.println("获取连接: " + conn);
// 5. 归还连接
pool1.returnConnection(conn);
pool1.printStatus();
System.out.println();
System.out.println("===== 枚举单例演示 =====");
DatabaseConnectionPoolEnum.INSTANCE.configure(
"jdbc:mysql://localhost:3306/orders", 10);
Connection enumConn = DatabaseConnectionPoolEnum.INSTANCE.getConnection();
System.out.println("获取连接: " + enumConn);
}
}
3.5 运行结果
text
===== DCL 单例演示 =====
pool1 == pool2: true
连接池配置完成: jdbc:mysql://localhost:3306/orders
连接池状态: 活跃=0/10 url=jdbc:mysql://localhost:3306/orders
获取连接: Connection{id=1, url='jdbc:mysql://localhost:3306/orders', user='root'}
归还连接: Connection{id=1, url='jdbc:mysql://localhost:3306/orders', user='root'}
连接池状态: 活跃=0/10 url=jdbc:mysql://localhost:3306/orders
===== 枚举单例演示 =====
枚举单例配置完成: jdbc:mysql://localhost:3306/orders
获取连接: Connection{id=1, url='jdbc:mysql://localhost:3306/orders', user='enumUser'}
四、单例模式的原理深度剖析
4.1 线程安全原理:DCL 完整执行时序
java
// 场景:线程A和线程B同时调用 getInstance()
// 时刻1:线程A 检查 instance == null → true
// 时刻2:线程A 获取锁(进入 synchronized 块)
// 时刻3:线程A 第二次检查 instance == null → true
// 时刻4:线程A 执行 instance = new DatabaseConnectionPool()
// - 分配内存
// - 初始化对象
// - 将引用赋给 instance(volatile 保证写操作对其他线程可见)
// 时刻5:线程A 释放锁
// 时刻6:线程B 获取锁
// 时刻7:线程B 第二次检查 instance == null → false(volatile 保证读到最新值)
// 时刻8:线程B 直接返回已创建好的实例 ✅
// 关键:如果没有 volatile
// 时刻4 的三步可能被重排序为:分配内存 → 赋值引用 → 初始化对象
// 线程B 在时刻1 的第一次检查中看到 instance != null(但对象未初始化完成)
// 直接返回半成品对象 → 💥 NPE 或数据错乱
4.2 类加载机制与单例
静态内部类单例依赖 JVM 的类加载机制实现线程安全,具体过程:
java
// 静态内部类单例
class ConnectionPoolHolder {
private ConnectionPoolHolder() {}
private static class Holder {
// 静态字段初始化在 <clinit>() 中执行
// JVM 保证 <clinit>() 同步执行,且只执行一次
private static final ConnectionPoolHolder INSTANCE = new ConnectionPoolHolder();
}
public static ConnectionPoolHolder getInstance() {
return Holder.INSTANCE; // 首次访问触发 Holder 类加载
}
}
// 类加载过程:
// 1. 外部类 ConnectionPoolHolder 被加载 → 不加载内部类 Holder
// 2. 调用 getInstance() → 引用 Holder.INSTANCE → 触发 Holder 类加载
// 3. JVM 加载 Holder 类 → 执行 <clinit>() → 创建 INSTANCE(线程安全)
// 4. 后续调用直接返回已创建的 INSTANCE
💡 为什么静态内部类比 DCL 更优雅?
DCL 需要手动写 volatile + 双重检查,容易写错(忘记 volatile 是最常见的 bug)。静态内部类把线程安全完全交给 JVM 保证,代码更简洁,且天然支持延迟加载。
五、单例模式的缺陷与防御
5.1 反射攻击
攻击演示
java
// 文件: ReflectionAttack.java
package com.devkit.patterns.creational.singleton;
import java.lang.reflect.Constructor;
/**
* 反射攻击演示:通过反射绕过私有构造方法,破坏单例
*/
public class ReflectionAttack {
public static void main(String[] args) throws Exception {
// 正常获取单例
DatabaseConnectionPool pool1 = DatabaseConnectionPool.getInstance();
// 通过反射获取私有构造方法
Constructor<DatabaseConnectionPool> constructor =
DatabaseConnectionPool.class.getDeclaredConstructor();
constructor.setAccessible(true);
// 通过反射创建新实例
DatabaseConnectionPool pool2 = constructor.newInstance();
System.out.println("pool1 == pool2: " + (pool1 == pool2)); // false!单例被破坏
}
}
防御方案 1:DCL + 独立标志位
直接在构造方法中检查 instance != null 是不够的------如果攻击者在 getInstance() 被调用之前就用反射创建,instance 仍为 null,防御形同虚设。正确的做法是使用一个独立标志位:
java
// 文件: DatabaseConnectionPool.java(含反射防御)
package com.devkit.patterns.creational.singleton;
public class DatabaseConnectionPool {
private static volatile DatabaseConnectionPool instance;
// ⭐ 独立标志位,不依赖 instance
private static volatile boolean initialized = false;
private DatabaseConnectionPool() {
// 防御反射攻击:无论 instance 是否为 null 都能拦截
synchronized (DatabaseConnectionPool.class) {
if (initialized) {
throw new RuntimeException(
"单例类不允许通过反射创建,请使用 getInstance()");
}
initialized = true;
}
}
public static DatabaseConnectionPool getInstance() {
if (instance == null) {
synchronized (DatabaseConnectionPool.class) {
if (instance == null) {
instance = new DatabaseConnectionPool();
// initialized 已在构造方法中被设为 true
}
}
}
return instance;
}
}
防御方案 2:静态内部类 + 独立标志位
静态内部类同样需要独立标志位。如果直接在构造方法中检查 SingletonHolder.INSTANCE != null,会触发自引用陷阱 :反射调用构造方法时访问 SingletonHolder.INSTANCE → 触发 Holder 类加载 → 执行 new ConnectionPoolManager() → 再次进入构造方法 → 循环依赖。
正确方案:
java
// 文件: ConnectionPoolManager.java(静态内部类 + 反射防御)
package com.devkit.patterns.creational.singleton;
public class ConnectionPoolManager {
// 独立标志位,不依赖 SingletonHolder.INSTANCE
private static volatile boolean initialized = false;
private ConnectionPoolManager() {
synchronized (ConnectionPoolManager.class) {
if (initialized) {
throw new RuntimeException(
"单例类不允许通过反射创建");
}
initialized = true;
}
initPool();
}
private static class SingletonHolder {
private static final ConnectionPoolManager INSTANCE = new ConnectionPoolManager();
}
public static ConnectionPoolManager getInstance() {
return SingletonHolder.INSTANCE;
}
private void initPool() {
// 初始化连接池资源
}
public Connection getConnection() {
return new Connection(0, "", "");
}
}
防御方案 3:使用枚举(最简单、最安全)
java
enum DatabaseConnectionPoolEnum {
INSTANCE;
// JVM 在 Constructor.newInstance() 层面拦截枚举类型
// 无需手动写任何防御代码
}
5.2 序列化攻击
攻击演示
java
// 文件: SerializationAttack.java
package com.devkit.patterns.creational.singleton;
import java.io.*;
/**
* 序列化攻击演示:序列化 + 反序列化会创建新对象,破坏单例
* 前提:单例类必须 implements Serializable
*/
public class SerializationAttack {
public static void main(String[] args) throws Exception {
DatabaseConnectionPool pool1 = DatabaseConnectionPool.getInstance();
// 序列化
try (ObjectOutputStream out = new ObjectOutputStream(
new FileOutputStream("pool.ser"))) {
out.writeObject(pool1);
}
// 反序列化
try (ObjectInputStream in = new ObjectInputStream(
new FileInputStream("pool.ser"))) {
DatabaseConnectionPool pool2 = (DatabaseConnectionPool) in.readObject();
System.out.println("pool1 == pool2: " + (pool1 == pool2)); // false!
}
}
}
防御方案:实现 readResolve() 方法
java
public class DatabaseConnectionPool implements Serializable {
private static final long serialVersionUID = 1L;
// ... 单例逻辑 ...
/**
* 反序列化时,JVM 会调用此方法。
* 返回单例实例,替代反序列化创建的新对象。
*/
private Object readResolve() {
return getInstance();
}
}
💡 readResolve 的工作原理
ObjectInputStream.readObject()在反序列化完成后,会检查目标类是否定义了readResolve()方法。如果有,就用该方法的返回值替换反序列化产生的对象。需要注意的是:反序列化仍然会创建一个临时对象(消耗资源),只是最终被丢弃 。这也是枚举单例更优的原因------枚举的反序列化直接通过Enum.valueOf()返回已有常量,根本不创建新对象。
5.3 克隆攻击
java
// 如果单例类实现了 Cloneable,clone() 可以创建副本
class BadSingleton implements Cloneable {
// ...
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone(); // 创建了新对象!单例被破坏
}
}
// ✅ 防御:不实现 Cloneable,或重写 clone()
class SafeSingleton {
@Override
protected Object clone() throws CloneNotSupportedException {
throw new CloneNotSupportedException("单例类不允许克隆");
// 或者: return getInstance();
}
}
六、单例模式是反模式吗?------ 工程视角的批判性分析
单例模式是 GoF 23 种设计模式中最简单的一种,但也是争议最大的一种。很多资深工程师和架构师认为单例模式在大多数场景下是反模式。原因如下:
6.1 全局状态问题
单例本质上是面向对象的全局变量 。任何代码都可以通过 getInstance() 访问它,这意味着:
- 隐藏了依赖关系:调用方代码看起来不依赖任何东西,实际上暗依赖单例
- 耦合难以追踪:无法从方法签名看出依赖了哪些单例
- 修改影响面大:单例状态被多处修改,出 bug 时难以定位是哪段代码改的
java
// ❌ 隐藏依赖:看起来没依赖,实际暗依赖 DatabaseConnectionPool
public class OrderService {
public void createOrder(Order order) {
// 谁能看出这里依赖了 DatabaseConnectionPool?
Connection conn = DatabaseConnectionPool.getInstance().getConnection();
// ...
}
}
// ✅ 显式依赖:通过构造方法注入,依赖关系一目了然
public class OrderService {
private final DatabaseConnectionPool pool;
// 依赖关系在构造方法中明确声明
public OrderService(DatabaseConnectionPool pool) {
this.pool = pool;
}
public void createOrder(Order order) {
Connection conn = pool.getConnection();
// ...
}
}
6.2 测试困难
单例的全局状态使得单元测试难以隔离:
java
// ❌ 单例难以 mock
public class OrderServiceTest {
@Test
void testCreateOrder() {
// DatabaseConnectionPool.getInstance() 返回的是真实单例
// 测试之间共享状态,无法隔离
// 无法替换为 mock 对象(除非用 PowerMock 等工具)
}
}
// ✅ 依赖注入使得测试轻松
public class OrderServiceTest {
@Test
void testCreateOrder() {
// 轻松注入 mock
DatabaseConnectionPool mockPool = mock(DatabaseConnectionPool.class);
when(mockPool.getConnection()).thenReturn(mockConnection);
OrderService service = new OrderService(mockPool);
service.createOrder(testOrder);
// 验证 mock 交互
verify(mockPool).getConnection();
}
}
6.3 违反单一职责原则
单例类同时承担了两个职责:
- 管理自身生命周期(保证只有一个实例)
- 业务逻辑(如连接池的连接管理)
这两个职责混在一起,违反了 SRP。
6.4 分布式环境下的局限性
GoF 单例保证的是单个 JVM 内只有一个实例。在分布式部署时,每个 JVM 进程各有一个实例,"单例"不再单:
text
应用节点1 (JVM-1) → DatabaseConnectionPool 实例 A
应用节点2 (JVM-2) → DatabaseConnectionPool 实例 B
应用节点3 (JVM-3) → DatabaseConnectionPool 实例 C
// 三个节点各有一个连接池实例,连接池配置可能不一致
// 如果单例持有计数器(如 activeConnections),各节点计数独立,无法汇总
6.5 现代 Java 的替代方案
| 场景 | 传统做法 | 现代替代方案 |
|---|---|---|
| Spring 应用 | 手写单例 | Spring 容器管理 Bean(@Component 默认单例) |
| 需要全局配置 | 单例 + 静态方法 | 依赖注入 + 配置类(@Configuration) |
| 需要全局缓存 | 单例 Map | 专门的缓存库(Caffeine、Redis) |
| 需要全局日志 | 单例 Logger | SLF4J 的 LoggerFactory(内部已处理) |
结论:什么时候手写单例是合理的?
- 不使用 Spring/IoC 容器的纯 Java 项目
- 需要精确控制实例创建时机的底层框架/库
- 面试和教学场景(理解设计模式原理)
在 Spring 项目中,优先用容器管理单例,不要手写。了解手写单例的原理仍然重要------它帮助你理解 Spring 容器底层在做什么。
七、单例模式在框架中的应用
7.1 Spring 框架中的单例
Spring 单例 vs GoF 单例
这是一个面试高频考点,两者是完全不同的东西:
| 维度 | GoF 单例 | Spring 单例 |
|---|---|---|
| 作用域 | 每个 ClassLoader 一个实例 | 每个 IoC 容器(ApplicationContext)一个实例 |
| 实现方式 | 私有构造 + 静态方法 | 容器管理 Bean 生命周期 |
| 构造方法 | 必须私有 | 可以是 public(容器通过反射调用) |
能否 new |
不能(私有构造) | 能(但容器管理的实例只有一个) |
| 防反射 | 需手动实现 | 不防(Spring 自己通过反射创建 Bean) |
| 防序列化 | 需手动实现 | 不防 |
| 多容器 | N/A | 两个 ApplicationContext 各有一个实例 |
java
// Spring 单例:构造方法可以是 public,容器通过反射创建
@Component
public class UserService {
// Spring 容器中只有一个 UserService 实例
// 但你仍然可以 new UserService()(不过拿不到容器注入的依赖)
}
// GoF 单例:构造方法必须私有
public class DatabaseConnectionPool {
private static volatile DatabaseConnectionPool instance;
private DatabaseConnectionPool() {} // 私有!
public static DatabaseConnectionPool getInstance() { ... }
}
Spring 单例的实现机制
java
// Spring 通过 ConcurrentHashMap 管理单例 Bean
// DefaultSingletonBeanRegistry.java(简化)
public class DefaultSingletonBeanRegistry {
// 单例池:beanName → Bean 实例
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);
public Object getSingleton(String beanName) {
// 先从缓存中取
Object bean = singletonObjects.get(beanName);
if (bean != null) {
return bean;
}
// 缓存没有 → 创建 → 放入缓存
bean = createBean(beanName);
singletonObjects.put(beanName, bean);
return bean;
}
}
7.2 MyBatis 中的单例
java
// SqlSessionFactory 通常配置为单例(通过 Spring 管理)
@Configuration
public class MyBatisConfig {
@Bean
public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
SqlSessionFactoryBean factory = new SqlSessionFactoryBean();
factory.setDataSource(dataSource);
return factory.getObject();
}
// @Bean 默认单例,整个应用只有一个 SqlSessionFactory
}
八、JDK 中的真实案例深度剖析
8.1 Runtime ------ 饿汉式单例 ✅
java
// JDK 源码: java.lang.Runtime(JDK 21)
public class Runtime {
private static final Runtime currentRuntime = new Runtime();
private Runtime() {}
public static Runtime getRuntime() {
return currentRuntime;
}
}
// → 标准的饿汉式单例,实现方式与本文 2.1 节完全一致
8.2 System ------ 工具类,不是单例
java
// JDK 源码: java.lang.System
public final class System {
// 私有构造,外部无法实例化
private System() {}
// 全部是静态方法和静态字段,没有任何实例
public static final PrintStream out = null;
public static final InputStream in = null;
public static void exit(int status) { ... }
public static long currentTimeMillis() { ... }
public static String getProperty(String key) { ... }
}
System 是工具类(Utility Class),不是单例。区别:
- 单例 :有且仅有一个实例 (通过
getInstance()获取实例) - 工具类 :根本没有实例(全是静态方法,私有构造防止实例化)
| 对比 | 单例模式 | 工具类 |
|---|---|---|
| 是否有实例 | 有,且只有一个 | 没有 |
| 状态 | 可以有状态 | 无状态 |
| 调用方式 | getInstance().method() |
ClassName.method() |
| 典型例子 | Runtime.getRuntime() |
System.exit()、Math.abs()、Collections.sort() |
8.3 Collections.emptyList() ------ 享元模式,不是单例
java
// JDK 源码: java.util.Collections
public class Collections {
@SuppressWarnings("rawtypes")
public static final List EMPTY_LIST = new EmptyList<>();
public static final <T> List<T> emptyList() {
return (List<T>) EMPTY_LIST;
}
}
Collections.emptyList() 返回的是同一个静态常量实例,但这更接近**享元模式(Flyweight Pattern)**而非单例模式。区别:
- 单例:一个类只有一个实例
- 享元:一个类可以有多个实例,但某些特定值(如空列表)共享同一个实例以节省内存
EmptyList 类本身可以被 new 出多个实例(虽然实际没必要),只是 Collections 恰好缓存了一个共享实例。这不符合单例"类只有一个实例"的定义。
8.4 JDK 单例/类单例案例汇总
| JDK 类 | 模式 | 说明 |
|---|---|---|
Runtime |
饿汉式单例 ✅ | 私有构造 + 静态 final 实例 + getRuntime() |
Desktop (java.awt) |
工厂方法 + 单例 | Desktop.isDesktopSupported() + Desktop.getDesktop() |
Logger (java.util.logging) |
容器管理(非严格单例) | LogManager 管理多个 Logger 实例,但每个 name 对应同一个实例 |
System |
工具类 ❌ 不是单例 | 私有构造,全静态方法,无实例 |
Collections.emptyList() |
享元模式 ❌ 不是单例 | 共享常量实例,但类本身可多实例 |
九、六种实现方式完整对比
| 实现方式 | 线程安全 | 延迟加载 | 性能 | 防御反射 | 防御序列化 | 复杂度 | 推荐度 |
|---|---|---|---|---|---|---|---|
| 饿汉式 | ✅ | ❌ | ⭐⭐⭐ | ❌ | ❌(需 readResolve) | ⭐ | ⭐⭐⭐ |
| 懒汉式(不安全) | ❌ | ✅ | ⭐⭐⭐ | ❌ | ❌ | ⭐ | ❌ 禁用 |
| 同步方法 | ✅ | ✅ | ⭐ | ❌ | ❌ | ⭐⭐ | ⭐⭐ |
| 双重检查锁(DCL) | ✅ | ✅ | ⭐⭐⭐ | ❌(需标志位) | ❌(需 readResolve) | ⭐⭐⭐ | ⭐⭐⭐⭐ |
| 静态内部类 | ✅ | ✅ | ⭐⭐⭐ | ❌(需标志位) | ❌(需 readResolve) | ⭐⭐ | ⭐⭐⭐⭐⭐ |
| 枚举 | ✅ | 类级延迟 ✅ | ⭐⭐⭐ | ✅ JVM 拦截 | ✅ Enum.valueOf | ⭐ | ⭐⭐⭐⭐⭐ |
💡 一句话推荐
- 不需要延迟加载 → 枚举(最安全,Effective Java 推荐)
- 需要延迟加载 → 静态内部类(最优雅,JVM 保证线程安全)
- 需要延迟加载 + 精确控制初始化时机 → DCL(需要 volatile)
- Spring 项目 → 不要手写,用
@Component
十、企业级最佳实践
10.1 完整的最佳实践示例
以下是一个结合了反射防御、序列化防御、克隆防御的静态内部类单例完整实现:
java
// 文件: com/devkit/patterns/creational/singleton/ConnectionPoolManager.java
package com.devkit.patterns.creational.singleton;
import java.io.Serializable;
/**
* 数据库连接池管理器 - 最佳实践
* 实现方式: 静态内部类 + 独立标志位反射防御 + readResolve 序列化防御
*
* 为什么不用枚举?
* - 枚举不能继承其他类(如果连接池需要继承 AbstractDataSource)
* - 枚举不能延迟初始化字段(类加载即初始化所有常量)
*/
public final class ConnectionPoolManager implements Serializable {
private static final long serialVersionUID = 1L;
// ⭐ 独立标志位,不依赖 SingletonHolder.INSTANCE
private static volatile boolean initialized = false;
// 业务字段
private String url;
private String username;
private int maxPoolSize;
private int activeConnections;
// 1. 私有构造方法 + 反射防御
private ConnectionPoolManager() {
synchronized (ConnectionPoolManager.class) {
if (initialized) {
throw new RuntimeException(
"单例类不允许通过反射创建,请使用 getInstance()");
}
initialized = true;
}
// 构造方法中不做重活,延迟到 configure()
}
// 2. 静态内部类持有实例
private static class SingletonHolder {
private static final ConnectionPoolManager INSTANCE = new ConnectionPoolManager();
}
// 3. 全局访问点
public static ConnectionPoolManager getInstance() {
return SingletonHolder.INSTANCE;
}
// 4. 业务配置方法
public void configure(String url, String username, int maxPoolSize) {
this.url = url;
this.username = username;
this.maxPoolSize = maxPoolSize;
this.activeConnections = 0;
System.out.println("连接池配置完成: " + url);
}
// 5. 防止序列化破坏
private Object readResolve() {
return SingletonHolder.INSTANCE;
}
// 6. 防止克隆
@Override
protected Object clone() throws CloneNotSupportedException {
throw new CloneNotSupportedException("单例类不允许克隆");
}
// 7. 业务方法
public Connection getConnection() {
if (activeConnections >= maxPoolSize) {
throw new RuntimeException("连接池已满");
}
activeConnections++;
return new Connection(activeConnections, url, username);
}
public void returnConnection(Connection conn) {
if (activeConnections > 0) {
activeConnections--;
}
}
public void printStatus() {
System.out.println("连接池状态: 活跃=" + activeConnections + "/" + maxPoolSize
+ " url=" + url);
}
}
10.2 常见问题与注意事项
Q1:单例类需要实现 Serializable 吗?
- 不需要序列化 → 不实现,省心
- 需要序列化 → 必须
implements Serializable+ 实现readResolve()
Q2:单例类可以有子类吗?
- 可以继承,但子类无法调用父类的私有构造方法
- 建议用
final修饰,防止继承
Q3:单例模式如何传参?
java
// 方案1:通过初始化方法传参(推荐)
ConnectionPoolManager.getInstance().configure(url, username, password);
// 方案2:通过配置文件
Properties props = new Properties();
props.load(new FileInputStream("db.properties"));
ConnectionPoolManager.getInstance().configure(
props.getProperty("db.url"),
props.getProperty("db.username"),
props.getProperty("db.password")
);
// 方案3:通过 Spring 注入(Spring 项目推荐)
@Component
public class PoolConfig {
@Value("${db.url}") private String url;
@Value("${db.username}") private String username;
@PostConstruct
public void init() {
ConnectionPoolManager.getInstance().configure(url, username, 10);
}
}
Q4:多个类加载器下的单例?
不同类加载器会创建不同的单例。解决方案:指定同一个类加载器,或使用 Spring 容器管理。
十一、面试高频问题及深度解答
Q1: 为什么要用 volatile?
答:两个作用------
- 禁止指令重排序 :
new对象分三步(分配内存→初始化→赋值引用),volatile 确保三步按序执行。没有 volatile,JVM 可能重排序为"分配→赋值→初始化",其他线程拿到未初始化的对象。 - 保证可见性:一个线程写入 instance 后,volatile 保证写操作刷新到主内存,其他线程读取时从主内存加载最新值,不会读到旧值。
Q2: 双重检查锁为什么要检查两次?
答:
- 第一次检查(无锁):如果实例已创建,直接返回,避免不必要的同步开销
- 第二次检查(有锁):多个线程可能同时通过第一次检查并排队获取锁。第一个线程创建实例后释放锁,第二个线程拿到锁时需要再次检查,否则会重复创建
Q3: 静态内部类方式为什么线程安全?
答 :JVM 的类加载机制保证 <clinit>()(类初始化方法)同步执行且只执行一次。多个线程同时触发内部类加载时,只有一个线程执行 <clinit>(),其他线程阻塞等待。
Q4: 枚举单例为什么最安全?
答:三个层面------
- 防反射 :JVM 在
Constructor.newInstance()源码层面显式拦截枚举类型 ,检查到Modifier.ENUM就抛IllegalArgumentException: Cannot reflectively create enum objects。 - 防序列化 :Java 序列化规范规定枚举通过
Enum.valueOf(name)反序列化,直接返回已有常量,不创建新对象。 - 线程安全 :枚举常量的初始化在类的
<clinit>()中完成,JVM 保证线程安全。
Q5: Spring 单例和 GoF 单例有什么区别?
答:
| 维度 | GoF 单例 | Spring 单例 |
|---|---|---|
| 作用域 | ClassLoader 级别 | IoC 容器级别 |
| 构造方法 | 私有 | 可以 public(容器通过反射创建) |
| 能否 new | 不能 | 能(但容器管理的只有一个) |
| 多容器 | N/A | 每个容器各一个实例 |
Q6: 单例模式和静态类(工具类)有什么区别?
| 对比 | 单例模式 | 静态类(工具类) |
|---|---|---|
| 是否有实例 | 有,且只有一个 | 没有 |
| 状态 | 可以有状态 | 通常无状态 |
| 继承/多态 | 可以实现接口、被继承 | 不能 |
| 延迟加载 | 支持 | 不支持 |
| 测试 mock | 可以(通过接口) | 困难 |
| 典型例子 | Runtime.getRuntime() |
System.exit()、Math.abs() |
Q7: 单例模式有什么缺点?什么场景下不该用?
答:单例本质是全局变量,主要缺点:
- 全局状态:隐藏依赖关系,增加耦合
- 测试困难:单例状态在测试间共享,难以隔离 mock
- 违反 SRP:既管生命周期又管业务逻辑
- 分布式失效:每个 JVM 一个实例,多节点不单
在 Spring 项目中,优先用容器管理单例(@Component),不要手写。
十二、现代 Java 视角
12.1 Java 8+:interface default method
Java 8 的 default method 对单例模式本身没有直接影响,但在抽象工厂模式中非常有用(给工厂接口加 default 方法,新增产品类型不破坏已有工厂)。单例模式更多受益于 Java 5 的 volatile 语义增强和 Java 9 的模块系统。
12.2 Record 类与单例
Java 16+ 引入的 record 是一种不可变的数据载体,天然 final:
java
// record 天然 final,不可继承,天然不可变
// 但 record 的构造方法默认是 public,不能设为 private
// 所以 record 不能直接用于实现单例
// ❌ 编译错误:record 不能有 private 构造方法
// public record MySingleton(String value) {
// private MySingleton {}
// }
// 如果需要不可变单例,仍然用 class + final + 私有构造
12.3 Sealed 类与单例
Java 17 的 sealed 可以控制继承层次,用于更精确的单例约束:
java
// sealed 类可以指定哪些类允许继承
// 这对单例的意义在于:可以创建一个"密封的单例基类"
// 只有指定的子类可以作为单例
public sealed class AbstractSingleton
permits DatabaseConnectionPool, CacheManager {
private static volatile boolean initialized = false;
protected AbstractSingleton() {
if (initialized) {
throw new RuntimeException("不允许反射创建");
}
initialized = true;
}
}
final class DatabaseConnectionPool extends AbstractSingleton { ... }
final class CacheManager extends AbstractSingleton { ... }
12.4 模块系统(JPMS)与单例
Java 9 的模块系统提供了更强的封装:
java
// module-info.java
module com.devkit.patterns {
// 只导出单例类的公共 API,不导出实现
exports com.devkit.patterns.creational.singleton;
}
// 在模块化项目中,即使构造方法是 public,
// 模块外的代码也无法通过反射访问未导出的包
// 这提供了额外的单例保护层
十三、总结与决策流程
13.1 核心要点回顾
text
单例模式三要素:
1. 私有构造方法
2. 静态变量保存实例
3. 静态方法提供访问点
实现方式选择:
Spring 项目 → @Component(容器管理,不要手写)
不需要延迟加载 → 枚举(最安全,Effective Java 推荐)
需要延迟加载 → 静态内部类(最优雅)
需要延迟加载 + 精确控制 → DCL(必须 volatile)
防御检查清单:
□ 反射防御(独立标志位 initialized)
□ 序列化防御(readResolve)
□ 克隆防御(throw CloneNotSupportedException)
□ final 修饰类(防止继承)
13.2 决策流程
text
是否需要单例?
├── Spring 项目 → 用 @Component,不要手写
├── 非 Spring 项目
│ ├── 需要延迟加载?
│ │ ├── 是 → 需要精确控制初始化时机?
│ │ │ ├── 是 → DCL(volatile + 双重检查)
│ │ │ └── 否 → 静态内部类(推荐)
│ │ └── 否 → 枚举(推荐,最安全)
│ └── 只有一个实例就够了?
│ └── 考虑用工具类(静态方法)代替,如果无状态
│
└── 不确定是否需要单例?
└── 大概率不需要。先正常写,出现"全局只需一个"的需求时再重构
13.3 一句话核心标准
你的产品类型是否稳定?你是否真的需要全局唯一实例?
- 在 Spring 项目中 → 用
@Component,永远不要手写单例- 不需要延迟加载 → 枚举(最安全)
- 需要延迟加载 → 静态内部类(最优雅)
- 不确定是否需要单例 → 大概率不需要
如果这篇文章对你有帮助,欢迎点赞、收藏、转发。有任何问题欢迎评论区讨论。