走进 Gang of Four 设计模式:单例模式

说明:不仅告诉你"怎么用",更告诉你"为什么这样设计"

前置知识:Java 面向对象基础(构造方法、static、synchronized)、多线程基础


📊 设计模式分类总览

GoF 23 种设计模式可按两个维度交叉分类:类/对象(处理方式) × 创建型/结构型/行为型(目的)。

维度创建型(Creational)结构型(Structural)行为型(Behavioral)
类(Class)
(通过继承复用)
Factory Method
工厂方法
Adapter(类适配器)Interpreter(解释器)
Template Method(模板方法)
对象(Object)
(通过组合/聚合复用)
Abstract Factory(抽象工厂)
Builder(建造者)
Prototype(原型)
Singleton(单例)
Adapter(对象适配器)
Bridge(桥接)
Composite(组合)
Decorator(装饰)
Facade(外观)
Flyweight(享元)
Proxy(代理)
Chain of Resp.(责任链)
Command(命令)
Iterator(迭代器)
Mediator(中介者)
Memento(备忘录)
Observer(观察者)
State(状态)
Strategy(策略)
Visitor(访问者)

本文档聚焦 创建型-对象模式 中的 Singleton(单例),其余创建型对象模式包括 Abstract Factory、Builder、Prototype。


📑 目录

  1. 模式概述
  2. 六种实现方式
  3. 六种实现对比总表
  4. Spring 单例分析
  5. 单例模式生态应用
  6. 总结

1. 模式概述

1.1 要解决什么问题?

核心矛盾:某些对象在系统中只需要一个实例(如配置管理器、连接池、日志工厂),但如果每次使用时都 new 一个,会导致资源浪费、状态不一致甚至系统错误。

// ❌ 反面教材:每次 new 一个,资源浪费且状态无法统一
ConfigManager cfg1 = new ConfigManager();
ConfigManager cfg2 = new ConfigManager();
cfg1.set("theme", "dark");
System.out.println(cfg2.get("theme")); // null —— 状态不同步!

解决方案:确保一个类只有一个实例,并提供全局访问点。

1.2 模式定义

单例模式确保一个类只有一个实例,并提供一个全局访问点来访问该实例。

这是 GoF 《设计模式》中定义的创建型模式,也是 Java 开发中最常用、也最容易用错的模式之一。

1.3 三个核心特征

特征说明
私有构造方法防止外部通过 new 创建实例
自我实例化类自己负责创建唯一实例(静态成员变量)
全局访问点提供静态方法 getInstance() 返回该实例

2. 六种实现方式

2.1 懒汉式(线程不安全)

public class Singleton {
    private static Singleton instance;

    private Singleton() {}

    public static Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}
维度评价
Lazy 初始化✅ 是
线程安全❌ 否(多线程下可能创建多个实例)
实现难度⭐ 易

风险:线程 A 执行到 if (instance == null) 后切换,线程 B 也执行到同一位置,两个线程会各自创建实例,破坏了单例。

2.2 懒汉式(线程安全)

public class Singleton {
    private static Singleton instance;

    private Singleton() {}

    public static synchronized Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}
维度评价
Lazy 初始化✅ 是
线程安全✅ 是(但代价大)
实现难度⭐ 易

问题synchronized 加在整个方法上,每次调用 getInstance() 都需要获取锁,即使实例已经创建。并发高时性能损失严重。

2.3 饿汉式

public class Singleton {
    private static final Singleton instance = new Singleton();

    private Singleton() {}

    public static Singleton getInstance() {
        return instance;
    }
}
维度评价
Lazy 初始化❌ 否(类加载时即创建)
线程安全✅ 是(类加载线程安全由 JVM 保证)
实现难度⭐ 易

优点:实现最简单,无锁、线程安全。
代价:类加载时就创建实例,如果该实例从未被使用,会造成内存浪费。

2.4 双检锁(DCL, Double-Checked Locking)

public class Singleton {
    private static volatile Singleton singleton;

    private Singleton() {}

    public static Singleton getSingleton() {
        if (singleton == null) {                         // 第一次检查
            synchronized (Singleton.class) {              // 加锁
                if (singleton == null) {                   // 第二次检查
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }
}
维度评价
Lazy 初始化✅ 是
线程安全✅ 是
实现难度⭐⭐⭐ 较复杂

关键细节

要素作用
第一次检查 if (singleton == null)避免实例已创建后不必要的加锁,提升性能
synchronized保证同一时间只有一个线程进入创建代码块
第二次检查 if (singleton == null)防止多个线程同时通过第一次检查后重复创建
volatile禁止指令重排序,确保 new Singleton() 的写操作对其他线程立即可见

为什么需要 volatile singleton = new Singleton() 在 JVM 中分为三步:① 分配内存 ② 初始化对象 ③ 将引用指向内存。如果发生指令重排序变成 ①→③→②,线程 B 在步骤③后看到引用不为 null,直接返回了未初始化完成的对象,导致使用时报错。volatile 禁止了这种重排序。

2.5 静态内部类(Holder 模式)

public class Singleton {
    private Singleton() {}

    private static class SingletonHolder {
        private static final Singleton INSTANCE = new Singleton();
    }

    public static Singleton getInstance() {
        return SingletonHolder.INSTANCE;
    }
}
维度评价
Lazy 初始化✅ 是(内部类在被调用时才加载)
线程安全✅ 是(类加载机制保证)
实现难度⭐⭐ 中

原理:JVM 在类加载阶段会执行 <clinit>() 方法,该方法由 JVM 自身保证同步。SingletonHolder 类只有在 getInstance() 方法中首次被引用时才会被 JVM 加载,此时才创建 INSTANCE,实现了延迟加载 + 线程安全 + 零同步开销的完美组合。

这是兼顾性能与简洁性的最佳实践,在不需要处理序列化/反射攻击的常规场景下优先推荐。

2.6 枚举

public enum Singleton {
    INSTANCE;

    public void whateverMethod() {
        // 业务方法
    }
}
维度评价
Lazy 初始化❌ 否
线程安全✅ 是
实现难度⭐ 易

优势(《Effective Java》作者 Josh Bloch 提倡):

防护维度说明
序列化安全枚举的序列化由 JVM 保证单例,不会像普通类那样反序列化时创建新实例
反射安全反射不能通过 Constructor.newInstance() 创建枚举实例
线程安全枚举实例的创建由 JVM 保证
代码简洁一行代码完成单例定义

如果需要绝对防止反射攻击和序列化破坏,枚举是实现单例模式的最佳方案


3. 六种实现对比总表

实现方式Lazy线程安全锁开销反射安全序列化安全推荐指数
懒汉式(非线程安全)
懒汉式(线程安全)每次调用⭐⭐
饿汉式⭐⭐⭐⭐
双检锁(DCL)首次创建⭐⭐⭐
静态内部类⭐⭐⭐⭐⭐
枚举⭐⭐⭐⭐⭐

选型建议

  • 常规项目优先选 静态内部类(简洁、线程安全、支持延迟加载)
  • 需要序列化/反射防护用 枚举
  • 无延迟加载需求且实现越简单越好用 饿汉式
  • JDK 自身源码如 Runtime 就用了饿汉式

4. Spring 单例分析

4.1 核心区别:作用域不同

这是经典单例与 Spring 单例最本质的区别:

对比维度经典单例模式Spring Singleton Bean
控制粒度限制在 ClassLoader 级别(一个 JVM 一个实例)限制在 Spring IoC 容器 级别(一个容器内一个 Bean ID 一个实例)
实现方式私有构造方法 + 静态方法(如 DCL、静态内部类)属性配置/注解(@Scope("singleton"))+ 单例注册表
实例化时机饿汉:类加载时;懒汉:首次调用 getInstance()默认容器启动时初始化(可 @Lazy 改为懒加载)
可测试性,构造方法私有,难以 Mock 和继承,普通 POJO,构造方法公开,方便单元测试
灵活性一个类在 JVM 中只能有一个实例同一个类可注册为多个不同名的 Bean(如多数据源)

4.2 Spring 的实现机制:单例注册表

Spring 没有使用私有构造方法来限制实例创建,而是使用单例注册表(Singleton Registry)—— 一个 ConcurrentHashMap

// Spring 内部核心逻辑简化示意
public class DefaultSingletonBeanRegistry {
    // 一级缓存:存放完整的、可用的单例 Bean
    private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);

    public Object getSingleton(String beanName, ObjectFactory<?> singletonFactory) {
        synchronized (this.singletonObjects) {
            Object singletonObject = this.singletonObjects.get(beanName);
            if (singletonObject == null) {
                singletonObject = singletonFactory.getObject(); // 通过反射创建
                this.singletonObjects.put(beanName, singletonObject);
            }
            return singletonObject;
        }
    }
}

流程:请求 Bean → 查缓存 → 有则直接返回 → 无则通过反射创建 → 放入缓存 → 返回。

这意味着 Spring 管理的 Bean 其构造方法通常是 public 的,单例的约束来自 IoC 容器的缓存管理,而不是 Java 语言层面的访问控制。

4.3 三级缓存与循环依赖

Spring 在 DefaultSingletonBeanRegistry 中定义了三级缓存,用于解决单例 Bean 之间的循环依赖问题(如 A 依赖 B,B 依赖 A)。

三级缓存结构
// 一级缓存:完全初始化好的 Bean(成品)
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);

// 二级缓存:早期暴露的半成品 Bean(属性尚未注入完毕)
private final Map<String, Object> earlySingletonObjects = new ConcurrentHashMap<>(16);

// 三级缓存:存放 ObjectFactory 工厂,用于生成早期 Bean 的代理对象
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);
缓存级别别名存放内容用途
一级singletonObjects完整初始化的 Bean 实例最终供客户端使用
二级earlySingletonObjects半成品 Bean(已实例化,未完成属性注入)提前暴露给依赖方
三级singletonFactoriesObjectFactory 工厂产生半成品或 AOP 代理对象
解决流程(A ↔ B 循环依赖)
1. 开始创建 A
   ├─ A 实例化(调用构造方法)→ 半成品
   ├─ 将 A 的 ObjectFactory 放入三级缓存
   ├─ 填充属性:发现需要 B
   │   └─ 去缓存找 B → 找不到 → 开始创建 B
   │
2. 开始创建 B
   ├─ B 实例化 → 半成品
   ├─ 将 B 的 ObjectFactory 放入三级缓存
   ├─ 填充属性:发现需要 A
   │   └─ 去缓存找 A
   │       ├─ 一级:没有
   │       ├─ 二级:没有
   │       └─ 三级:找到 A 的 ObjectFactory → 执行 getObject()
   │           ├─ 如果 A 需要 AOP:返回代理对象
   │           └─ 否则返回原始半成品
   │       → 将结果放入二级缓存,从三级缓存移除
   │       → B 成功注入 A(此时 A 还是半成品)
   ├─ B 初始化完成 → 放入一级缓存
   │
3. 回到 A
   ├─ A 成功注入 B
   ├─ A 初始化完成 → 放入一级缓存
   └─ 从二级/三级缓存移除
为什么需要三级,两级不够?

关键原因:AOP(动态代理)的生命周期管理。

  • 如果只有两级缓存,Bean 在实例化后必须马上创建代理对象放入二级缓存,但 Spring 的设计原则是 AOP 代理应在 Bean 初始化阶段末尾通过 BeanPostProcessor 创建。
  • 三级缓存引入 ObjectFactory,实现了延迟创建代理:没有循环依赖时,三级缓存不会被触发,AOP 代理按正常生命周期在初始化后创建;只有发生循环依赖时,依赖方才会触发三级缓存提前生成代理。

一句话:三级缓存不是必需的,AOP 才是三级缓存的原因。

4.4 @Lazy 原理

@Lazy 是 Spring 提供的延迟加载机制,分为两种场景:

场景一:单独使用
@Component
@Lazy
public class HeavyService {
    public HeavyService() {
        // 容器启动时不创建,首次 getBean 时才创建
    }
}

Spring 扫描到 @Lazy 后,在容器刷新的最后阶段(finishBeanFactoryInitialization)会跳过该 Bean 的提前实例化,等到代码中第一次调 getBean() 或注入时才触发创建。

场景二:注入时使用(动态代理)
@Component
public class AService {
    @Autowired
    @Lazy
    private BService bService; // B 是懒加载的
}

此时 A 需要注入 B,但 B 又是懒加载不能现在创建,Spring 的做法是:

  1. 不为 B 创建真实对象,而是用 CGLIB 或 JDK 动态代理生成一个 B 的空壳代理对象
  2. 把这个代理对象注入给 A 的 bService 字段
  3. 当 A 执行 bService.doSomething() 时,代理拦截方法调用
  4. 代理去容器中检查 B 是否已创建,若未创建则触发 getBean("bService") 完成真实创建
  5. 将方法调用转发给真实的 B 实例

这是"善意的欺骗"——注入的是一个代理壳子,把对象的真正创建延迟到了方法首次被调用的瞬间。

@Lazy 解决构造器循环依赖

三级缓存只能解决 Setter/属性注入 的循环依赖。对于构造器注入的循环依赖,三级缓存也无力回天(因为构造器调用时 Bean 还没实例化,放不进三级缓存)。

此时 @Lazy 可以破局:

@Component
public class AService {
    public AService(@Lazy BService bService) { // 构造器注入 + @Lazy
        this.bService = bService;
    }
}

Spring 看到 @Lazy 后,直接注入一个 B 的代理壳子到构造器,B 本身并不创建。A 成功实例化,循环依赖破解。


5. 单例模式生态应用

单例模式的核心理念——全局唯一、资源共享——在 Java 生态中无处不在。

5.1 JDK 源码中的经典单例

// Runtime —— 饿汉式单例
public class Runtime {
    private static final Runtime currentRuntime = new Runtime();
    public static Runtime getRuntime() {
        return currentRuntime;
    }
    private Runtime() {}
}
JDK 类实现方式用途
Runtime.getRuntime()饿汉式获取 Java 运行时环境
Desktop.getDesktop()双检锁(DCL)获取系统桌面(浏览器/邮件)
System.console()单次检查获取控制台对象

5.2 日志框架

private static final Logger log = LoggerFactory.getLogger(XxxService.class);

LoggerFactory 内部通过单例注册表模式维护 LoggerContext

  • 同一个 Class 对应的 Logger 实例全局唯一
  • 保证日志级别变更能全局同步
  • Logback、Log4j2 均采用此设计

5.3 数据库连接池

@Bean
public DataSource dataSource() {
    return DataSourceBuilder.create()
        .url("jdbc:mysql://localhost:3306/db")
        .build();
}
  • 连接池本身(HikariCP、Druid) 在 Spring 容器中是单例 Bean
  • 如果不是单例,会创建多个连接池,耗尽数据库连接资源
  • 连接池内部管理的 Connection 对象则可以有多条(池化复用)

5.4 中间件客户端

中间件单例对象原因
RedisJedisPool / LettuceConnectionFactory管理网络长连接,全局唯一
RocketMQDefaultMQProducer与 Broker 搭建复杂路由,高代价初始化
RabbitMQConnectionFactory维护连接池,防止资源浪费
ZooKeeperZooKeeper 客户端实例维护会话状态,全局一致性

5.5 Spring 框架内部

组件角色
ApplicationContext全局唯一的 IoC 容器上下文
Environment管理所有配置来源(properties、环境变量)
BeanPostProcessor干预所有 Bean 生命周期的全局处理器

6. 总结

核心要点

  • 单例模式的本质:控制一个类在特定范围内的实例数量为 1,提供全局访问点
  • 经典单例 vs Spring 单例:前者在 ClassLoader 级别通过私有构造实现,后者在 IoC 容器级别通过注册表缓存实现
  • 六种实现推荐顺序:静态内部类 ≈ 枚举 > 饿汉式 > DCL > 懒汉式(同步) > 懒汉式(非同步)

选型建议

场景推荐实现
常规 Java 项目静态内部类
需要序列化/反射防护枚举
无延迟加载需求、追求简单饿汉式
JDK 源码场景(如 Runtime)饿汉式
Spring 管理 Bean直接用 @Component + @Scope("singleton"),不要手写单例

最终一句话

在现代 Java 企业级开发中,尽量少自己动手写经典单例模式。把对象的生命周期和单例控制交给 Spring IoC 容器 去管理,既能享受单例带来的资源节约,又能保持代码的松耦合和高可测性。理解单例模式的价值在于读懂框架源码,而不是在业务代码中四处手动实现它。


Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐