欢迎光临
我们一直在努力

【单例模式从入门到精通:一个数据库连接池的完整剖析】

单例模式从入门到精通:一个数据库连接池的完整剖析

本文通过一个真实的数据库连接池场景,从理论到实践完整讲解单例模式,包含 6 种实现方式对比、线程安全分析、反射攻击与防御、单例反模式批判性分析、Spring 单例 vs GoF 单例辨析、面试高频问题及企业级最佳实践。


目录

  • 写在前面:为什么需要单例模式?
  • 单例模式的 6 种实现方式
  • 数据库连接池完整代码实战
  • 单例模式的原理深度剖析
  • 单例模式的缺陷与防御(反射 / 序列化 / 克隆)
  • 单例模式是反模式吗?—— 工程视角的批判性分析
  • 单例模式在框架中的应用(含 Spring 单例 vs GoF 单例辨析)
  • JDK 中的真实案例深度剖析
  • 六种实现方式完整对比
  • 企业级最佳实践
  • 面试高频问题及深度解答
  • 现代 Java 视角:record / sealed / 模块系统
  • 总结与决策流程

  • 一、写在前面:为什么需要单例模式?

    1.1 一个真实的业务痛点

    在开发企业级应用时,我们经常遇到这样的场景:

    // ❌ 问题代码:每次创建新的连接池
    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)

    // 文件: 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 懒汉式(线程不安全)

    // 文件: 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;
    }
    }

    问题演示:

    线程A: if (instance == null) → true → 暂停
    线程B: if (instance == null) → true → 创建实例
    线程A: 继续执行 → 创建另一个实例 → 单例失效!

    ⚠️ 实际项目中绝对不要用这种写法。 仅作为理解"为什么需要线程安全"的反面教材。

    2.3 懒汉式(同步方法)

    // 文件: 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)⭐

    // 文件: 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?

    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 静态内部类(最优雅)⭐

    // 文件: 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 枚举单例(最安全)

    // 文件: 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() 源码层面显式拦截了枚举类型:

    // 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 项目结构

    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 类

    // ===== 文件: 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 单例完整实现

    // ===== 文件: 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 测试入口

    // ===== 文件: 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 运行结果

    ===== 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 完整执行时序

    // 场景:线程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 的类加载机制实现线程安全,具体过程:

    // 静态内部类单例
    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 反射攻击

    攻击演示

    // 文件: 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,防御形同虚设。正确的做法是使用一个独立标志位:

    // 文件: 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() → 再次进入构造方法 → 循环依赖。

    正确方案:

    // 文件: 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:使用枚举(最简单、最安全)

    enum DatabaseConnectionPoolEnum {
    INSTANCE;
    // JVM 在 Constructor.newInstance() 层面拦截枚举类型
    // 无需手动写任何防御代码
    }

    5.2 序列化攻击

    攻击演示

    // 文件: 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() 方法

    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 克隆攻击

    // 如果单例类实现了 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 时难以定位是哪段代码改的

    // ❌ 隐藏依赖:看起来没依赖,实际暗依赖 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 测试困难

    单例的全局状态使得单元测试难以隔离:

    // ❌ 单例难以 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 进程各有一个实例,"单例"不再单:

    应用节点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 各有一个实例

    // 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 单例的实现机制

    // 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 中的单例

    // 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 —— 饿汉式单例 ✅

    // 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 —— 工具类,不是单例

    // 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() —— 享元模式,不是单例

    // 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 完整的最佳实践示例

    以下是一个结合了反射防御、序列化防御、克隆防御的静态内部类单例完整实现:

    // 文件: 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:单例模式如何传参?

    // 方案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:

    // record 天然 final,不可继承,天然不可变
    // 但 record 的构造方法默认是 public,不能设为 private
    // 所以 record 不能直接用于实现单例

    // ❌ 编译错误:record 不能有 private 构造方法
    // public record MySingleton(String value) {
    // private MySingleton {}
    // }

    // 如果需要不可变单例,仍然用 class + final + 私有构造

    12.3 Sealed 类与单例

    Java 17 的 sealed 可以控制继承层次,用于更精确的单例约束:

    // 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 的模块系统提供了更强的封装:

    // module-info.java
    module com.devkit.patterns {
    // 只导出单例类的公共 API,不导出实现
    exports com.devkit.patterns.creational.singleton;
    }

    // 在模块化项目中,即使构造方法是 public,
    // 模块外的代码也无法通过反射访问未导出的包
    // 这提供了额外的单例保护层


    十三、总结与决策流程

    13.1 核心要点回顾

    单例模式三要素:
    1. 私有构造方法
    2. 静态变量保存实例
    3. 静态方法提供访问点

    实现方式选择:
    Spring 项目 → @Component(容器管理,不要手写)
    不需要延迟加载 → 枚举(最安全,Effective Java 推荐)
    需要延迟加载 → 静态内部类(最优雅)
    需要延迟加载 + 精确控制 → DCL(必须 volatile)

    防御检查清单:
    □ 反射防御(独立标志位 initialized)
    □ 序列化防御(readResolve)
    □ 克隆防御(throw CloneNotSupportedException)
    □ final 修饰类(防止继承)

    13.2 决策流程

    是否需要单例?
    ├── Spring 项目 → 用 @Component,不要手写
    ├── 非 Spring 项目
    │ ├── 需要延迟加载?
    │ │ ├── 是 → 需要精确控制初始化时机?
    │ │ │ ├── 是 → DCL(volatile + 双重检查)
    │ │ │ └── 否 → 静态内部类(推荐)
    │ │ └── 否 → 枚举(推荐,最安全)
    │ └── 只有一个实例就够了?
    │ └── 考虑用工具类(静态方法)代替,如果无状态

    └── 不确定是否需要单例?
    └── 大概率不需要。先正常写,出现"全局只需一个"的需求时再重构

    13.3 一句话核心标准

    你的产品类型是否稳定?你是否真的需要全局唯一实例?

    • 在 Spring 项目中 → 用 @Component,永远不要手写单例
    • 不需要延迟加载 → 枚举(最安全)
    • 需要延迟加载 → 静态内部类(最优雅)
    • 不确定是否需要单例 → 大概率不需要

    如果这篇文章对你有帮助,欢迎点赞、收藏、转发。有任何问题欢迎评论区讨论。

    赞(0)
    未经允许不得转载:171主机测评 » 【单例模式从入门到精通:一个数据库连接池的完整剖析】
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址