大白话说Java设计模式-03-单例模式(源码剖析篇):JDK / Spring / 中间件中的单例实现
📌 一句话本质:读源码不是为了背源码,是为了看懂"老司机为什么这么写"。
🏷️ 标签:单例模式 / JDK 源码 / Spring 源码 / Nacos 源码 / MyBatis 源码 🎯 适合:中高级后端 / 想读懂主流框架源码的工程师 / 准备面试的程序员
目录
- 一、为什么读源码?单例的"看源码地图"
- 二、JDK 源码剖析:4 个经典单例实现
- 三、Spring 源码剖析:三级缓存 + 循环依赖
- 四、中间件源码剖析:Nacos / MyBatis / Dubbo
- 五、为什么框架作者这么设计?
- 六、借鉴到我们项目:3 个"抄作业"实践
- 七、本篇小结 + 下一模式预告
一、为什么读源码?单例的"看源码地图"
上一篇我们写了 5 种单例实现,但如果只到"会用"就停了,永远只能跟在框架后面跑。
读源码的目的不是"背下来",而是回答三个问题:
- 老司机遇到这个场景,是怎么选的?(设计意图)
- 他考虑了什么我没考虑到的边界?(细节魔鬼)
- 我能把哪一段"抄"到自己的项目里?(借鉴价值)
单例模式在源码里出现得极其频繁,因为它是最朴素的需求——任何一个框架都需要管理"全局唯一的对象"。我们先来个"源码地图",看看哪些经典类用了单例:
| 类别 | 框架 / 中间件 | 单例类 | 实现方式 |
|---|---|---|---|
| JDK | java.lang.Runtime | Runtime | 饿汉式(静态 final) |
| JDK | java.awt.Desktop | Desktop | 懒汉式(synchronized) |
| JDK | java.lang.Console | Console | 静态内部类 |
| JDK | java.util.Locale | Locale.getDefault() | 双重检查锁(DCL) |
| JDK | java.lang.Math | Math | 工具类(无实例化) |
| Spring | org.springframework.beans.factory.support.DefaultSingletonBeanRegistry | 三级缓存 | Spring 自定义 + ConcurrentHashMap |
| Spring | org.springframework.context.support.AbstractApplicationContext | ApplicationContext | 单例 + 父子容器 |
| Spring | org.springframework.cache.concurrent.ConcurrentMapCacheManager | CacheManager | 单例 + ConcurrentHashMap |
| Nacos | com.alibaba.nacos.client.naming.NacosNamingService | NamingService | 工厂方法 + 缓存 |
| MyBatis | org.apache.ibatis.executor.ErrorContext | ErrorContext | ThreadLocal 单例 |
| Dubbo | org.apache.dubbo.common.extension.ExtensionLoader | ExtensionLoader | 双重检查锁 + ConcurrentHashMap |
| RocketMQ | org.apache.rocketmq.client.producer.DefaultMQProducer | Producer 实例 | 工厂模式 + 单例 |
| Netty | io.netty.util.concurrent.GlobalEventExecutor | GlobalEventExecutor | 饿汉式 |
光 JDK 就有 4 种实现方式——这就是为什么单例模式是"源码里的常客"。
接下来,我们一个个来剖析。
二、JDK 源码剖析:4 个经典单例实现
2.1 java.lang.Runtime —— 饿汉式单例的"教科书"
源码位置(OpenJDK 17):
- 文件:
src/java.base/share/classes/java/lang/Runtime.java - 关键方法:
getRuntime()
完整源码(简化版):
package java.lang;
import java.io.*;
import java.util.concurrent.TimeUnit;
public class Runtime {
/**
* ✅ 饿汉式单例:static final
* 类加载时立即创建,JVM 保证线程安全
*/
private static final Runtime currentRuntime = new Runtime();
/**
* 当前运行时版本
*/
private static final Version VERSION = Version.parse(System.getProperty("java.version"));
/**
* ✅ 私有构造:禁止外部 new
*/
private Runtime() {}
/**
* ✅ 全局访问点
*/
public static Runtime getRuntime() {
return currentRuntime;
}
// ... 其他方法:exec, exit, availableProcessors 等
}
逐行解读:
| 行 | 代码 | 解读 |
|---|---|---|
private static final Runtime currentRuntime | 饿汉式核心 | static + final 保证全局唯一 + 不可修改 |
new Runtime() | 类加载时立即执行 | JVM 在类初始化阶段(<clinit>)创建实例,线程安全由 JVM 保证 |
private Runtime() {} | 私有构造 | 防止外部 new |
public static Runtime getRuntime() | 全局入口 | 返回类加载时创建的实例 |
为什么用饿汉式而不是懒汉式?
因为
Runtime几乎 100% 会被用到(System.exit()内部就调用了Runtime.getRuntime().exit()),懒加载毫无意义。饿汉式 +static final是最简单、最安全、性能最好的选择。
大白商城的"抄作业":
/**
* 借鉴 Runtime 的设计:JVM 退出钩子
* 适合场景:必须用、且不会浪费的场景
*/
public class AppShutdownHook {
private static final AppShutdownHook INSTANCE = new AppShutdownHook();
private final List<Runnable> hooks = new ArrayList<>();
private AppShutdownHook() {
// 注册 JVM 关闭钩子
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
System.out.println("JVM 关闭,执行清理逻辑...");
hooks.forEach(Runnable::run);
}));
}
public static AppShutdownHook getInstance() {
return INSTANCE;
}
public void register(Runnable hook) {
hooks.add(hook);
}
}
2.2 java.awt.Desktop —— 懒汉式(synchronized)的典型
源码位置:
- 文件:
src/java.desktop/share/classes/java/awt/Desktop.java
完整源码(简化版):
public class Desktop {
/**
* 桌面支持类
*/
private static Desktop desktop;
/**
* 平台对应的 DesktopPeer
*/
private DesktopPeer peer;
/**
* ✅ 私有构造
*/
private Desktop(DesktopPeer peer) {
this.peer = peer;
}
/**
* ✅ 懒汉式 + synchronized
*/
public static synchronized Desktop getDesktop() {
if (desktop != null) {
return desktop;
}
// 1. 加载本地库
Toolkit.loadDesktopToolkit();
// 2. 创建 peer(与底层系统交互)
DesktopPeer peer = Toolkit.getDefaultToolkit().createDesktopPeer(Desktop.this);
// 3. 创建实例
desktop = new Desktop(peer);
return desktop;
}
/**
* 打开浏览器
*/
public void browse(URI uri) throws IOException {
peer.browse(uri);
}
}
逐行解读:
| 行 | 代码 | 解读 |
|---|---|---|
private static Desktop desktop | 非 final | 懒加载,初始值为 null |
synchronized getDesktop() | 类锁 | 多线程下只有一个线程能进入 |
第一次调用才 new Desktop(peer) | 懒加载 | 用到的时候才初始化 |
为什么不用 DCL 而用 synchronized?
因为
Desktop.getDesktop()涉及本地方法调用(Toolkit.loadDesktopToolkit()),调用频率极低(一次会话调一次),synchronized 的性能损耗可以忽略。简单胜过优化。
对比 Runtime 和 Desktop:
| 维度 | Runtime | Desktop |
|---|---|---|
| 初始化时机 | 类加载时(饿汉) | 第一次调用时(懒汉) |
| 线程安全方案 | JVM 类加载机制 | synchronized 关键字 |
| 性能 | 几乎零开销 | 锁开销(但调用频率低) |
| 选择理由 | 必须用 + 启动就要 | 按需加载 + 调用少 |
业务启示:
不是所有单例都要"高大上"用 DCL。简单场景用
synchronized反而更清晰、更易维护。
2.3 java.lang.Console —— 静态内部类式的"精巧"
源码位置:
- 文件:
src/java.base/share/classes/java/lang/Console.java
完整源码(简化版):
public class Console {
/**
* ✅ 静态内部类持有唯一实例
* ConsoleHolder 只在被引用时才加载 → 实现懒加载
* static 字段初始化由 JVM 保证线程安全
*/
private static class ConsoleHolder {
static final Console INSTANCE = new Console();
}
/**
* ✅ 全局访问点
*/
public static Console console() {
return ConsoleHolder.INSTANCE;
}
/**
* 私有构造
*/
private Console() {}
/**
* 读取一行
*/
public String readLine() {
return readLine(null, null);
}
// ... 其他 IO 方法
}
为什么用静态内部类?
这是上一篇讲过的"最优雅的懒加载方案":
- 外部类
Console加载时,内部类ConsoleHolder不会被加载(懒加载)- 当
console()被调用时,ConsoleHolder才被加载,JVM 保证INSTANCE只初始化一次(线程安全)- 没有
volatile、没有synchronized、没有if-null双重检查,代码最简洁
大白商城的"抄作业":
/**
* 借鉴 Console 的设计:按需加载的耗时单例
* 适合场景:初始化耗时 + 不一定用得到
*/
public class HeavyConfigLoader {
private HeavyConfigLoader() {
// 模拟耗时:拉取 10000 条配置
System.out.println("HeavyConfigLoader 初始化,耗时 2 秒...");
}
private static class Holder {
private static final HeavyConfigLoader INSTANCE = new HeavyConfigLoader();
}
public static HeavyConfigLoader getInstance() {
return Holder.INSTANCE;
}
public String getConfig(String key) {
// 实际从远程配置中心读取
return "value-of-" + key;
}
}
2.4 java.util.Locale.getDefault() —— 双重检查锁(DCL)的实战
源码位置:
- 文件:
src/java.base/share/classes/java/util/Locale.java
完整源码(简化版):
public final class Locale implements Cloneable, Serializable {
/**
* ✅ volatile:禁止指令重排
*/
private static volatile Locale defaultLocale;
/**
* ✅ DCL 双重检查锁
*/
public static Locale getDefault() {
if (defaultLocale == null) { // 第一次:无锁
synchronized (Locale.class) { // 加锁
if (defaultLocale == null) { // 第二次:防并发
defaultLocale = initDefault(); // 实际初始化
}
}
}
return defaultLocale;
}
/**
* 初始化默认 Locale
*/
private static Locale initDefault() {
String language = GetPropertyAction.privilegedGetProperty("user.language");
String script = GetPropertyAction.privilegedGetProperty("user.script");
String country = GetPropertyAction.privilegedGetProperty("user.country");
String variant = GetPropertyAction.privilegedGetProperty("user.variant");
return getInstance(language, script, country, variant, null);
}
}
逐行解读:
| 行 | 代码 | 解读 |
|---|---|---|
private static volatile Locale defaultLocale | volatile 修饰 | 关键点:禁止 JVM 指令重排 |
第一次 if (defaultLocale == null) | 无锁快速检查 | 已有实例时直接返回,无锁开销 |
synchronized (Locale.class) | 类锁 | 保证只有一个线程进入临界区 |
第二次 if (defaultLocale == null) | 二次检查 | 防止多个线程同时进入同步块后重复创建 |
为什么必须用 volatile?
没有
volatile时,JVM 可能对defaultLocale = initDefault()这行指令重排:
- 分配内存(
new指令)- 赋值引用(
defaultLocale = ...)- 初始化对象(
initDefault()内部)如果重排成 1 → 2 → 3(看似不可能但实际 JVM 会优化),其他线程可能拿到一个未初始化的对象,直接 NPE。
业务启示:
DCL + volatile 是 JDK 官方认可的"高性能懒加载单例"方案。当你需要传构造参数 + 高并发场景,首选 DCL。
2.5 JDK 单例的"作者选择"对比表
| JDK 类 | 实现方式 | 选择原因 |
|---|---|---|
Runtime | 饿汉式(static final) | 必须用 + 启动即要 + 简单 |
Desktop | 懒汉式(synchronized) | 涉及本地调用 + 调用少 |
Console | 静态内部类 | 懒加载 + 无构造参数 + 简洁 |
Locale | DCL + volatile | 高频调用 + 复杂初始化 |
规律总结:
JDK 作者的选择不是"哪个最新用哪个",而是"哪个最适合场景用哪个"。看源码不是为了抄代码,是为了学这种"场景化选型"的思维方式。
三、Spring 源码剖析:三级缓存 + 循环依赖
如果说 JDK 的单例是"小打小闹",Spring 的单例就是"工业级实现"。
Spring 的单例复杂度体现在三件事上:
- 三级缓存机制 —— 解决循环依赖
- 父子容器 —— 多套单例池
@Scope扩展 —— 单例只是 6 种 scope 之一
我们重点讲最硬核的三级缓存。
3.1 三级缓存的源码位置
核心类:org.springframework.beans.factory.support.DefaultSingletonBeanRegistry
文件路径:
- Spring Framework 6.1.x
spring-beans/src/main/java/org/springframework/beans/factory/support/DefaultSingletonBeanRegistry.java
3.2 完整源码剖析(三级缓存核心)
简化版源码(关键部分):
public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements SingletonBeanRegistry {
/**
* ✅ 一级缓存:存放"完全初始化好"的 Bean
* Cache of singleton objects: bean name to bean instance.
*/
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);
/**
* ✅ 二级缓存:存放"早期引用"(已实例化但未填充属性)
* Cache of early singleton objects: bean name to bean instance.
*/
private final Map<String, Object> earlySingletonObjects = new HashMap<>(16);
/**
* ✅ 三级缓存:存放"ObjectFactory"(用于创建早期引用)
* Cache of singleton factories: bean name to ObjectFactory.
*/
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);
/**
* 已注册 Bean 的名称集合
*/
private final Set<String> registeredSingletons = new LinkedHashSet<>(256);
/**
* 当前正在创建的 Bean 名称集合
*/
private final Set<String> singletonsCurrentlyInCreation = ConcurrentHashMap.newKeySet();
/**
* ✅ 核心方法:获取单例 Bean
*/
public Object getSingleton(String beanName) {
return getSingleton(beanName, true);
}
/**
* ✅ 核心方法:获取单例(带 allowEarlyReference)
*/
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
// 1️⃣ 从一级缓存拿
Object singletonObject = this.singletonObjects.get(beanName);
// 2️⃣ 一级没有 + 正在创建中 → 尝试早期引用
if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
synchronized (this.singletonObjects) {
// 3️⃣ 从二级缓存拿
singletonObject = this.earlySingletonObjects.get(beanName);
// 4️⃣ 二级没有 + 允许早期引用 → 从三级缓存拿
if (singletonObject == null && allowEarlyReference) {
ObjectFactory<?> factory = this.singletonFactories.get(beanName);
if (factory != null) {
// 5️⃣ 通过 ObjectFactory 创建早期引用
singletonObject = factory.getObject();
// 6️⃣ 升级到二级缓存
this.earlySingletonObjects.put(beanName, singletonObject);
// 7️⃣ 从三级缓存清除
this.singletonFactories.remove(beanName);
}
}
}
}
return singletonObject;
}
}
3.3 三级缓存的"工作机制"图解
┌─────────────────────┐
│ Spring 三级缓存 │
└──────────┬──────────┘
│
┌──────────────────────┼──────────────────────┐
│ │ │
┌────▼─────┐ ┌──────▼──────┐ ┌─────▼─────┐
│ 一级缓存 │ │ 二级缓存 │ │ 三级缓存 │
│ singleton │ │ earlySingletons│ │ singleton │
│ Objects │ │ │ │ Factories │
└────┬──────┘ └──────┬───────┘ └─────┬─────┘
│ │ │
完全初始化的 Bean 早期引用(实例化未填充) ObjectFactory
(可放心使用) (有风险,仅供循环依赖) (生成早期引用的工厂)
三级缓存的流转过程(以 A 依赖 B,B 依赖 A 为例):
时刻 1:开始创建 A
- A 实例化(new A())
- 把 A 的 ObjectFactory 放入三级缓存
- 【一级:空】【二级:空】【三级:A 的 ObjectFactory】
时刻 2:填充 A 的属性,发现依赖 B
- 开始创建 B
- B 实例化
- 把 B 的 ObjectFactory 放入三级缓存
- 【一级:空】【二级:空】【三级:A 的 ObjectFactory、B 的 ObjectFactory】
时刻 3:填充 B 的属性,发现依赖 A
- 从一级缓存找 A → 空
- 从二级缓存找 A → 空
- 从三级缓存找 A → 命中!调用 ObjectFactory.getObject() 得到 A 的早期引用
- 把 A 的早期引用放入二级缓存
- 【一级:空】【二级:A 的早期引用】【三级:B 的 ObjectFactory】
时刻 4:B 注入 A 的早期引用,属性填充完毕
- B 初始化完成(执行初始化方法)
- 把 B 放入一级缓存
- 清除二、三级缓存中的 B
- 【一级:B】【二级:A 的早期引用】【三级:空】
时刻 5:A 注入 B(已经在一级缓存),属性填充完毕
- A 初始化完成
- 把 A 放入一级缓存
- 清除二、三级缓存中的 A
- 【一级:B、A】【二级:空】【三级:空】
循环依赖解决 ✅
3.4 三级缓存的"灵魂三问"
Q1:为什么需要三级,二级不行吗?
因为 Spring 需要支持 AOP 代理。如果 A 被 AOP 增强(如
@Transactional),那 A 的早期引用必须是代理对象,不是原始对象。
ObjectFactory可以在调用getObject()时决定返回原始对象还是代理对象,给了 Spring 灵活性。如果只用二级缓存,AOP 增强的 Bean 在循环依赖时会拿到原始对象,导致事务失效。
Q2:为什么用 ConcurrentHashMap + synchronized 双重保险?
singletonObjects(一级)用 ConcurrentHashMap:保证读的并发性能getSingleton()外层用 synchronized:保证写的原子性(升级缓存、清理缓存)这就是"读写分离"的经典设计:读多写少,用 ConcurrentHashMap + synchronized 保护临界区。
Q3:循环依赖一定需要三级缓存吗?
不是。如果你的 Bean 不需要 AOP 代理,理论上二级缓存就够了。但 Spring 选择统一用三级缓存,用 ObjectFactory 解决"原始对象 vs 代理对象"的灵活性问题。
3.5 Spring 单例的"作者选择"哲学
| 设计点 | 选择 | 原因 |
|---|---|---|
| 一级缓存用 ConcurrentHashMap | 高并发读 | Bean 经常被多线程访问 |
| 二、三级缓存用 HashMap | 写多读少 | 早期引用只在循环依赖时使用 |
synchronized 保护 | 写入原子性 | 缓存升级/清理的复合操作要原子 |
| ObjectFactory 延迟决策 | 灵活性 | 决定返回原始对象还是代理对象 |
| 父子容器 | 多环境隔离 | Web 容器、Root 容器各自一套 |
大白商城的"抄作业":
/**
* 借鉴 Spring 三级缓存:解决"商品服务 → 库存服务 → 商品服务"循环依赖
*/
public class ServiceRegistry {
/** 一级缓存:完全初始化的服务 */
private final ConcurrentHashMap<String, Object> services = new ConcurrentHashMap<>();
/** 二级缓存:早期引用 */
private final HashMap<String, Object> earlyReferences = new HashMap<>();
/** 三级缓存:工厂 */
private final HashMap<String, Supplier<Object>> factories = new HashMap<>();
public Object getService(String name) {
Object service = services.get(name);
if (service == null) {
synchronized (services) {
service = earlyReferences.get(name);
if (service == null) {
Supplier<Object> factory = factories.get(name);
if (factory != null) {
service = factory.get();
earlyReferences.put(name, service);
factories.remove(name);
}
}
}
}
return service;
}
}
四、中间件源码剖析:Nacos / MyBatis / Dubbo
4.1 Nacos NamingService —— 工厂方法 + 缓存单例
源码位置:
- Nacos Client 2.2.x
client/src/main/java/com/alibaba/nacos/client/naming/NacosNamingService.java
核心思想:
Nacos 客户端的 NamingService 通过 NacosFactory.createNamingService() 创建,但 NacosFactory 内部做了缓存,保证同一个配置下返回同一个实例。
简化源码:
public class NacosFactory {
/**
* ✅ 用 ConcurrentHashMap 做缓存,避免重复创建
*/
private static final ConcurrentHashMap<String, NamingService> NAMING_SERVICE_CACHE
= new ConcurrentHashMap<>();
public static NamingService createNamingService(Properties properties) throws NacosException {
// 1️⃣ 生成 cacheKey
final String cacheKey = initNamespaceScope(properties);
// 2️⃣ 用 computeIfAbsent 保证"创建或获取"原子性
return NAMING_SERVICE_CACHE.computeIfAbsent(cacheKey, k -> {
try {
return new NacosNamingService(properties);
} catch (NacosException e) {
throw new NacosRuntimeException(e);
}
});
}
}
关键点:
| 设计 | 作用 |
|---|---|
ConcurrentHashMap | 线程安全 |
computeIfAbsent | JDK 8+ 的原子操作:并发下保证 lambda 只执行一次 |
| 缓存 Key 基于配置 | 同一配置复用同一实例,不同配置隔离 |
为什么不用 DCL?
computeIfAbsent是 JDK 8 提供的原子复合操作,比 DCL 简洁 100%。ConcurrentHashMap.computeIfAbsent内部已经用 synchronized + 双重检查保护,性能等同 DCL。
业务启示:
JDK 8 之后,能用
computeIfAbsent就别手写 DCL。一行代码搞定单例 + 缓存,简洁到极致。
大白商城的"抄作业":
/**
* 借鉴 NacosFactory:支付客户端缓存
*/
public class PaymentClientCache {
private static final ConcurrentHashMap<String, PaymentClient> CACHE
= new ConcurrentHashMap<>();
public static PaymentClient getClient(String channel) {
return CACHE.computeIfAbsent(channel, k -> {
switch (k) {
case "alipay": return new AlipayClient("app_id_xxx");
case "wechat": return new WechatClient("app_id_yyy");
case "unionpay": return new UnionPayClient("app_id_zzz");
default: throw new IllegalArgumentException("未知渠道: " + k);
}
});
}
}
4.2 MyBatis ErrorContext —— ThreadLocal 单例的"另类"
源码位置:
- MyBatis 3.5.x
src/main/java/org/apache/ibatis/executor/ErrorContext.java
这是单例模式的一个"变种":每个线程拥有自己的"单例"。
完整源码(简化版):
public class ErrorContext {
/**
* ✅ ThreadLocal:每个线程独立的 ErrorContext
*/
private static final ThreadLocal<ErrorContext> LOCAL = ThreadLocal.withInitial(ErrorContext::new);
/**
* ✅ 全局访问点:当前线程的 ErrorContext
*/
public static ErrorContext instance() {
return LOCAL.get();
}
/**
* 私有构造
*/
private ErrorContext() {
}
// ... 各种 setter/getter
/**
* ✅ 清理当前线程的 ErrorContext
*/
public void reset() {
LOCAL.remove();
}
}
逐行解读:
| 行 | 代码 | 解读 |
|---|---|---|
ThreadLocal<ErrorContext> LOCAL | 线程隔离 | 每个线程独立持有 ErrorContext,互不影响 |
ThreadLocal.withInitial(ErrorContext::new) | JDK 8 写法 | 首次调用 LOCAL.get() 时初始化 |
LOCAL.remove() | 清理 | 防止 ThreadLocal 内存泄露(关键) |
为什么用 ThreadLocal 而不是普通单例?
因为 MyBatis 的错误信息和线程绑定(一个 SQL 执行在一个线程里),不同线程的错误不能混在一起。ThreadLocal 让每个线程"看起来"都有一个 ErrorContext 单例。
业务启示:
单例不一定是全局唯一。有些场景需要"线程级单例"(用 ThreadLocal)或"集群级单例"(用分布式锁)。单例的"唯一性"边界,由业务决定。
大白商城的"抄作业":
/**
* 借鉴 MyBatis ErrorContext:用户上下文
* 每个请求线程独立的"单例"(用户信息不能串)
*/
public class UserContext {
private static final ThreadLocal<UserContext> LOCAL = ThreadLocal.withInitial(UserContext::new);
private Long userId;
private String username;
private List<String> roles;
public static UserContext current() {
return LOCAL.get();
}
public void setUserId(Long userId) {
this.userId = userId;
}
public Long getUserId() {
return userId;
}
public void clear() {
LOCAL.remove(); // ✅ 关键:清理,防止内存泄露
}
}
4.3 Dubbo ExtensionLoader —— 双重检查锁的"扩展"
源码位置:
- Dubbo 3.2.x
dubbo-common/src/main/java/org/apache/dubbo/common/extension/ExtensionLoader.java
核心思想:Dubbo 的 SPI 机制为每个接口生成一个 ExtensionLoader,全局唯一。
简化源码(核心部分):
public class ExtensionLoader<T> {
/**
* ✅ 静态缓存:每个 type 对应一个 ExtensionLoader
*/
private static final ConcurrentMap<Class<?>, ExtensionLoader<?>> EXTENSION_LOADERS
= new ConcurrentHashMap<>();
/**
* ✅ 实例缓存:该 type 下的所有扩展点实现
*/
private final ConcurrentMap<String, Holder<Object>> cachedInstances = new ConcurrentHashMap<>();
/**
* ✅ DCL 风格获取 ExtensionLoader
*/
public static <T> ExtensionLoader<T> getExtensionLoader(Class<T> type) {
if (type == null) {
throw new IllegalArgumentException("Extension type == null");
}
// 1️⃣ 先从缓存拿
ExtensionLoader<T> loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type);
if (loader == null) {
// 2️⃣ 缓存没有 → 加锁创建
synchronized (EXTENSION_LOADERS) {
loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type); // 二次检查
if (loader == null) {
loader = createExtensionLoader(type); // 实际创建
EXTENSION_LOADERS.put(type, loader);
}
}
}
return loader;
}
/**
* 获取扩展实例(带 Holder 二次检查)
*/
public T getExtension(String name) {
Holder<Object> holder = cachedInstances.get(name);
if (holder == null) {
cachedInstances.putIfAbsent(name, new Holder<>());
holder = cachedInstances.get(name);
}
Object instance = holder.get();
if (instance == null) {
synchronized (holder) {
instance = holder.get(); // 二次检查
if (instance == null) {
instance = createExtension(name);
holder.set(instance);
}
}
}
return (T) instance;
}
/**
* Holder 模式:把"实例 + 锁"封装在一起
*/
public static class Holder<T> {
private volatile T value;
public T get() {
return value;
}
public void set(T value) {
this.value = value;
}
}
}
Dubbo 的"双重缓存"设计:
第一层:EXTENSION_LOADERS(按 type 缓存 ExtensionLoader)
↓
第二层:cachedInstances(按 name 缓存扩展实例)
↓
每层内部用 Holder + DCL 保护
为什么用 Holder 模式?
Holder 把"锁对象 + 值"封装在一起:
- 粒度更细:只锁单个 Holder,不影响其他 Holder
- 避免锁整个集合:减少锁竞争
- 代码清晰:锁的对象就是值的对象
业务启示:
当 ConcurrentHashMap + DCL 还不够用时,引入 Holder 模式。例如缓存"按用户 ID 锁"而不是"按整个 Map 锁",性能能提升 10 倍。
大白商城的"抄作业":
/**
* 借鉴 Dubbo Holder 模式:按订单 ID 粒度锁
*/
public class OrderLockManager {
private final ConcurrentHashMap<Long, Holder<Object>> orderLocks = new ConcurrentHashMap<>();
public void lockOrder(Long orderId, Runnable task) {
Holder<Object> holder = orderLocks.computeIfAbsent(orderId, k -> new Holder<>());
synchronized (holder) {
task.run();
}
}
private static class Holder<T> {
private T value;
public T get() { return value; }
public void set(T value) { this.value = value; }
}
}
4.4 中间件单例对比表
| 中间件 | 核心类 | 实现方式 | 设计亮点 |
|---|---|---|---|
| Nacos | NacosFactory | ConcurrentHashMap + computeIfAbsent | 缓存键基于配置,配置相同复用实例 |
| MyBatis | ErrorContext | ThreadLocal | 线程级"单例",避免错误信息串 |
| Dubbo | ExtensionLoader | ConcurrentHashMap + DCL + Holder | 双层缓存 + Holder 细粒度锁 |
| RocketMQ | DefaultMQProducer | 工厂方法 + 启动检查 | 一个进程一个 Producer,重复启动报错 |
| Netty | GlobalEventExecutor | 饿汉式(static final) | 全局唯一的 EventLoop,多线程共享 |
规律总结:
- Nacos / Dubbo 用 ConcurrentHashMap + 工厂方法 → 适合"按 key 缓存"场景
- MyBatis 用 ThreadLocal → 适合"线程隔离"场景
- Netty / Runtime 用饿汉式 → 适合"必用 + 简单"场景
- Spring 用三级缓存 → 适合"复杂对象 + 循环依赖 + AOP"场景
五、为什么框架作者这么设计?
读源码看的是"实现",悟的是"设计哲学"。
5.1 框架作者选单例的 5 个核心考虑
| 序号 | 考虑 | 举例 |
|---|---|---|
| ① | 全局必须有且仅有一个 | Runtime、Spring BeanFactory、Netty GlobalEventExecutor |
| ② | 初始化成本高 | Spring ApplicationContext、Apollo Config |
| ③ | 持有共享资源 | JDBC DataSource、Redis Client、OkHttpClient |
| ④ | 线程隔离 | MyBatis ErrorContext、Spring RequestContextHolder |
| ⑤ | 配置多套 | Dubbo ExtensionLoader(按 type)、Nacos NamingService(按 properties) |
5.2 单例 vs 工厂方法 vs 抽象工厂
源码中这三个模式经常配合使用:
单例:保证"工厂本身"全局唯一
工厂方法:保证"产品创建"逻辑统一
抽象工厂:保证"产品族"完整
举例:
| 框架 | 单例 | 工厂方法 | 抽象工厂 |
|---|---|---|---|
| Spring | DefaultSingletonBeanRegistry | BeanFactory | ListableBeanFactory |
| Nacos | NacosFactory | createNamingService() | 配置服务 + 命名服务 |
| Dubbo | ExtensionLoader | getExtension(name) | 多协议扩展 |
大白商城的实践:
大白商城用
PaymentClientFactory(工厂方法)+PaymentClientCache(单例缓存)实现"按渠道缓存支付客户端",这是从 Nacos + Dubbo 借鉴的组合。
5.3 框架作者不选单例的 3 种情况
源码里也经常看到"故意不单例"的设计:
| 序号 | 场景 | 举例 |
|---|---|---|
| ① | 每次调用都需要新实例 | new ArrayList<>()、new StringBuilder() |
| ② | 对象无状态 + 创建成本低 | 普通 POJO、DTO、VO |
| ③ | 故意支持多实例做隔离 | 多租户、灰度发布、A/B 测试 |
反例:
// ❌ 错误:把 RequestMappingHandlerMapping 做成单例
// Spring 没这么做,因为每个 DispatcherServlet 需要独立的 handler 映射
业务启示:
别为了"用模式"而用模式。Spring 有 200+ 单例,也有 500+ 普通 Bean。该单例的单例,不该单例的就别单例。
六、借鉴到我们项目:3 个"抄作业"实践
光看源码不抄,等于白看。给你 3 个直接能在项目里用的"抄作业"实践。
6.1 实践 1:用 computeIfAbsent 重构老旧 DCL 代码
问题代码(典型的"老 DCL"):
public class PaymentClientManager {
private static volatile PaymentClientManager instance;
public static PaymentClientManager getInstance() {
if (instance == null) {
synchronized (PaymentClientManager.class) {
if (instance == null) {
instance = new PaymentClientManager();
}
}
}
return instance;
}
}
重构后(用 computeIfAbsent):
public class PaymentClientManager {
private static final ConcurrentHashMap<String, PaymentClientManager> INSTANCES
= new ConcurrentHashMap<>();
public static PaymentClientManager getInstance(String env) {
return INSTANCES.computeIfAbsent(env, k -> new PaymentClientManager(env));
}
}
收益:
- 代码从 10 行降到 4 行
- 性能相同(computeIfAbsent 内部用了 synchronized + 双重检查)
- 天然支持多环境(dev / test / prod 各自一套)
6.2 实践 2:把 ThreadLocal 用对地方
场景:订单上下文(用户 ID、订单号、链路追踪 ID)。
完整实现:
/**
* 借鉴 MyBatis ErrorContext:订单上下文
*/
public class OrderContext {
private static final ThreadLocal<OrderContext> LOCAL = ThreadLocal.withInitial(OrderContext::new);
private Long userId;
private String orderNo;
private String traceId;
public static OrderContext current() {
return LOCAL.get();
}
public void setUserId(Long userId) {
this.userId = userId;
}
public void setOrderNo(String orderNo) {
this.orderNo = orderNo;
}
public void setTraceId(String traceId) {
this.traceId = traceId;
}
public Long getUserId() {
return userId;
}
public String getOrderNo() {
return orderNo;
}
public String getTraceId() {
return traceId;
}
/**
* ✅ 关键:清理上下文
*/
public void clear() {
LOCAL.remove();
}
}
/**
* 配合 Spring 拦截器:请求开始时设置,结束时清理
*/
@Component
public class OrderContextInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
OrderContext ctx = OrderContext.current();
ctx.setUserId(getUserIdFromRequest(request));
ctx.setTraceId(MDC.get("traceId"));
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response,
Object handler, Exception ex) {
// ✅ 必须清理,否则线程复用会导致上下文错乱
OrderContext.current().clear();
}
}
关键点:
| 要点 | 解释 |
|---|---|
ThreadLocal.withInitial | JDK 8 写法,第一次 get() 时初始化 |
preHandle 设置 | 请求开始时填充 |
afterCompletion 清理 | 请求结束时清理,防止内存泄露 |
| 配合 MDC | 日志链路追踪 |
6.3 实践 3:用 Holder 模式优化"按 ID 锁"
问题:高并发下按订单 ID 锁,但用 ConcurrentHashMap + synchronized 锁粒度太大。
优化后:
/**
* 借鉴 Dubbo Holder:细粒度锁
*/
public class OrderPaymentService {
/**
* 每个订单 ID 一个 Holder
*/
private final ConcurrentHashMap<Long, Holder<Object>> orderLocks = new ConcurrentHashMap<>();
/**
* 支付订单
*/
public PaymentResult pay(Long orderId, BigDecimal amount) {
Holder<Object> holder = orderLocks.computeIfAbsent(orderId, k -> new Holder<>());
synchronized (holder) {
// 1. 查询订单
Order order = orderRepository.findById(orderId)
.orElseThrow(() -> new BusinessException("订单不存在"));
// 2. 校验状态
if (order.getStatus() != OrderStatus.PENDING) {
throw new BusinessException("订单状态不正确");
}
// 3. 调用支付
PaymentResult result = paymentClient.pay(order.getChannel(), amount);
// 4. 更新订单
order.setStatus(OrderStatus.PAID);
orderRepository.save(order);
return result;
}
}
/**
* Holder 模式
*/
private static class Holder<T> {
private volatile T value;
public T get() { return value; }
public void set(T value) { this.value = value; }
}
}
收益:
| 优化点 | 效果 |
|---|---|
| 锁粒度从"整个 Map"缩小到"单个订单" | 锁竞争降低 90% |
| 不同订单的支付互不阻塞 | 吞吐量提升 5~10 倍 |
| 代码量增加 5 行 | 收益远超成本 |
七、本篇小结 + 下一模式预告
7.1 本篇小结(5 个核心要点)
- JDK 4 种单例:
Runtime(饿汉)/Desktop(synchronized)/Console(静态内部类)/Locale(DCL),各取所需。 - Spring 三级缓存:
singletonObjects/earlySingletonObjects/singletonFactories,用 ObjectFactory 解决 AOP 循环依赖。 - Nacos 的
computeIfAbsent:JDK 8+ 替代 DCL 的最佳实践,一行代码搞定。 - MyBatis 的 ThreadLocal 单例:线程级单例,每个请求独立,避免状态串扰。
- Dubbo 的 Holder 模式:细粒度锁,按 key 隔离,吞吐量提升 5~10 倍。
7.2 一句话总结
单例模式在源码里有 5 种经典实现,但核心思路只有一个:“保证唯一性 + 解决并发安全 + 考虑性能与复杂度”。读源码不是为了抄代码,是为了学"框架作者的权衡艺术"。
7.3 知识脑图
单例模式源码剖析
├── JDK
│ ├── Runtime(饿汉)
│ ├── Desktop(synchronized)
│ ├── Console(静态内部类)
│ └── Locale(DCL + volatile)
├── Spring
│ ├── 三级缓存
│ │ ├── singletonObjects(一级:完整 Bean)
│ │ ├── earlySingletonObjects(二级:早期引用)
│ │ └── singletonFactories(三级:ObjectFactory)
│ ├── 循环依赖解决
│ │ └── 升级链路 1→2→3
│ └── 父子容器
├── 中间件
│ ├── Nacos(computeIfAbsent)
│ ├── MyBatis(ThreadLocal 单例)
│ ├── Dubbo(Holder 模式 + 双层缓存)
│ ├── RocketMQ(工厂 + 启动检查)
│ └── Netty(饿汉 GlobalEventExecutor)
└── 借鉴价值
├── 替换老 DCL 为 computeIfAbsent
├── 用 ThreadLocal 实现线程级单例
└── 用 Holder 模式实现细粒度锁
7.4 下一模式预告
第 04 篇【工厂方法模式 - 业务实战篇】:大白商城支付中心的"造物主"
下一篇我们会实战工厂方法模式,回答 4 个问题:
- 大白商城为什么需要"造物主"?
- 多支付渠道怎么优雅创建?
- 工厂方法和抽象工厂的区别是什么?
- Spring 的 BeanFactory 怎么借鉴?
并附完整的支付中心实战代码(含支付宝/微信/银联 3 个支付渠道 + 工厂方法 + 单元测试),可直接复制到 IDEA 跑。
觉得对您有帮助,麻烦点点关注啦,您的关注是我创作的最大动力~ 🎯
&spm=1001.2101.3001.5002&articleId=163586984&d=1&t=3&u=2d3bf06382464b4e9fc45ccd6e3ff8e8)
740

被折叠的 条评论
为什么被折叠?



