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

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

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


目录

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

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

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 的两个作用

  1. 禁止指令重排序:确保"分配→初始化→赋值"三步按序执行
  2. 保证可见性:一个线程修改 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>()

  1. 外部类 ConnectionPoolHolder 被加载时,不会 加载内部类 Holder
  2. 调用 getInstance() → 引用 Holder.INSTANCE → 触发 Holder 类加载
  3. JVM 加载 Holder 类 → 执行静态初始化 → 创建 INSTANCE(此过程线程安全)
  4. 后续调用直接返回已创建好的 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 违反单一职责原则

单例类同时承担了两个职责:

  1. 管理自身生命周期(保证只有一个实例)
  2. 业务逻辑(如连接池的连接管理)

这两个职责混在一起,违反了 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?

:两个作用------

  1. 禁止指令重排序new 对象分三步(分配内存→初始化→赋值引用),volatile 确保三步按序执行。没有 volatile,JVM 可能重排序为"分配→赋值→初始化",其他线程拿到未初始化的对象。
  2. 保证可见性:一个线程写入 instance 后,volatile 保证写操作刷新到主内存,其他线程读取时从主内存加载最新值,不会读到旧值。

Q2: 双重检查锁为什么要检查两次?

  • 第一次检查(无锁):如果实例已创建,直接返回,避免不必要的同步开销
  • 第二次检查(有锁):多个线程可能同时通过第一次检查并排队获取锁。第一个线程创建实例后释放锁,第二个线程拿到锁时需要再次检查,否则会重复创建

Q3: 静态内部类方式为什么线程安全?

:JVM 的类加载机制保证 <clinit>()(类初始化方法)同步执行且只执行一次。多个线程同时触发内部类加载时,只有一个线程执行 <clinit>(),其他线程阻塞等待。

Q4: 枚举单例为什么最安全?

:三个层面------

  1. 防反射JVM 在 Constructor.newInstance() 源码层面显式拦截枚举类型 ,检查到 Modifier.ENUM 就抛 IllegalArgumentException: Cannot reflectively create enum objects
  2. 防序列化 :Java 序列化规范规定枚举通过 Enum.valueOf(name) 反序列化,直接返回已有常量,不创建新对象。
  3. 线程安全 :枚举常量的初始化在类的 <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,永远不要手写单例
  • 不需要延迟加载 → 枚举(最安全)
  • 需要延迟加载 → 静态内部类(最优雅)
  • 不确定是否需要单例 → 大概率不需要

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

相关推荐
胡萝卜术1 小时前
编译期与运行期的双重防线:从 TypeScript 类型之争到 LLM 输出的自动化择优
前端·设计模式·面试
货拉拉技术1 小时前
重塑 Agent 度量衡:基于 LLM-as-a-Judge 的离线评估体系与实践
算法·设计模式
KhalilRuan2 小时前
设计模式小记
设计模式
卡牌RWA研究院2 小时前
Relique:一张精品卡牌如何从收藏品变成可交易的链上资产?
设计模式·金融·区块链·创业创新
剧中有戏1 天前
抽象工厂模式从入门到精通:一个多云存储案例的完整剖析(修订版)
设计模式
MC皮蛋侠客1 天前
Redis 系列(八):缓存设计模式与一致性——从 Cache Aside 到防雪崩
redis·缓存·设计模式
莫得感情 o1 天前
设计模式 18 · 状态模式
设计模式·状态模式
莫得感情 o1 天前
设计模式 17 · 责任链模式
设计模式·责任链模式
用户938515635072 天前
从零在浏览器里跑 DeepSeek-R1:WebGPU + Transformer.js 全链路实战
前端·设计模式·typescript