文章概述
单例模式(Singleton Pattern)是软件设计模式中最基础也最经典的一种创建型模式,其核心定义简洁而明确:确保一个类在整个JVM运行期间有且仅有一个实例,并提供一个全局访问点来获取该实例。尽管概念简单,但在Java并发编程、类加载机制、序列化协议以及分布式架构的复杂环境中,实现一个"真正"的线程安全、防反射攻击、防序列化破坏且具备集群唯一性的单例,却需要开发者对JVM内存模型、锁机制、类加载生命周期乃至分布式协调服务有深刻理解。
单例模式要解决的两大核心问题:一是实例数量的绝对控制 ------无论多少个线程并发访问、无论通过何种方式获取,始终返回同一个对象;二是全局访问入口的统一------为分散在各处的业务代码提供一个一致的状态共享点。这两个目标的达成与否直接影响到系统资源利用率、数据一致性乃至整体架构的健壮性。
本文将从最基础的六种Java单例实现方式入手,结合JVM指令层分析、内存屏障语义与锁升级过程,深入剖析饿汉式、懒汉式、DCL、Holder模式及枚举单例的底层原理。随后通过反射与序列化的PoC攻击代码,揭示单例的脆弱面并展示终极防御手段。进一步,我们将穿梭于JDK、Spring、MyBatis、Logback等经典框架源码,探寻单例模式在工业级产品中的精妙运用。当视角从单机扩展到分布式集群,类加载器隔离与跨进程唯一性的挑战又为单例赋予了新的内涵。最后,通过适用场景梳理、面试难题拆解与可视化流程图,为您呈现一幅完整的单例模式知识图谱。让我们一同深入这场从JVM指令到分布式锁的技术之旅。
一、代码实现:六种单例写法及JVM底层原理全剖析
以下所有实现均提供完整可运行的Demo代码,注释逐行解释设计意图与底层原理。环境基于JDK 8+,建议读者在阅读时同步执行以加深理解。
1. 饿汉式(静态常量与静态代码块)
饿汉式是最简单的单例实现,核心思想是在类加载阶段就完成实例的初始化,利用JVM类加载机制的线程安全性保证单例的唯一性。
java
/**
* 饿汉式 - 静态常量实现
*
* 底层原理剖析:
* 1. 类加载时机:当类被主动使用时触发初始化(new、反射、访问静态字段/方法等)
* 2. JVM在类加载的初始化阶段(<clinit>方法)会为静态变量赋值,
* 该过程是线程安全的,由JVM内部加锁保证
* 3. 实例在类加载时创建,若从未使用会造成内存浪费,但实现简单
*/
public class EagerSingletonStatic {
// 1. 私有构造器,防止外部new实例
private EagerSingletonStatic() {
System.out.println("EagerSingletonStatic 实例化");
}
// 2. 静态常量持有唯一实例,类加载时立即初始化
// static final 保证了变量的不可变性(引用不可变)
private static final EagerSingletonStatic INSTANCE = new EagerSingletonStatic();
// 3. 全局访问点
public static EagerSingletonStatic getInstance() {
return INSTANCE;
}
// 测试Main方法
public static void main(String[] args) {
EagerSingletonStatic s1 = EagerSingletonStatic.getInstance();
EagerSingletonStatic s2 = EagerSingletonStatic.getInstance();
System.out.println(s1 == s2); // true
}
}
java
/**
* 饿汉式 - 静态代码块实现
*
* 与静态常量方式的区别:初始化逻辑可以更复杂,比如从配置文件读取参数
* 底层原理:静态代码块同样在类初始化阶段由JVM在<clinit>方法中按顺序执行,
* 执行过程是线程安全的。
*/
public class EagerSingletonBlock {
private static final EagerSingletonBlock INSTANCE;
static {
// 静态代码块中可以执行复杂的初始化逻辑
System.out.println("静态代码块执行,类正在初始化...");
// 此处可以读取配置、处理异常等
INSTANCE = new EagerSingletonBlock();
}
private EagerSingletonBlock() {
System.out.println("EagerSingletonBlock 实例化");
}
public static EagerSingletonBlock getInstance() {
return INSTANCE;
}
public static void main(String[] args) {
EagerSingletonBlock s1 = EagerSingletonBlock.getInstance();
EagerSingletonBlock s2 = EagerSingletonBlock.getInstance();
System.out.println(s1 == s2);
}
}
饿汉式总结:实现简单,天然线程安全,但缺乏懒加载特性。在单例对象初始化耗时长且不一定会被使用的场景下,会拖慢应用启动速度并浪费内存。适合单例对象较小、一定会被使用的场景。
2. 懒汉式(线程不安全版)
懒汉式将实例化延迟到第一次调用getInstance()时,实现了懒加载,但未考虑并发场景。
java
/**
* 懒汉式 - 线程不安全版
*
* 问题演示:多线程并发调用getInstance()时,可能产生多个实例
* 底层原理:多个线程同时进入 if (instance == null) 判断时,
* 都认为实例未创建,从而各自执行new操作
*/
public class LazySingletonUnsafe {
private static LazySingletonUnsafe instance;
private LazySingletonUnsafe() {
// 模拟耗时初始化,放大并发问题出现概率
try { Thread.sleep(100); } catch (InterruptedException e) {}
System.out.println(Thread.currentThread().getName() + " 创建了实例");
}
public static LazySingletonUnsafe getInstance() {
if (instance == null) { // 线程不安全检查点
instance = new LazySingletonUnsafe();
}
return instance;
}
// 测试并发问题
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(() -> {
LazySingletonUnsafe.getInstance();
}, "Thread-" + i).start();
}
// 输出将显示多个线程创建了实例,证明单例被破坏
}
}
3. 懒汉式(同步方法)
通过在getInstance()方法上添加synchronized关键字解决并发问题,但带来了性能瓶颈。
java
/**
* 懒汉式 - 同步方法版
*
* 性能分析:每次调用getInstance()都需要获取类锁,即使实例已创建,
* 高并发下会成为系统瓶颈。锁升级过程:
* 无锁 -> 偏向锁(仅单线程访问) -> 轻量级锁(少量竞争) -> 重量级锁(竞争激烈)
*/
public class LazySingletonSyncMethod {
private static LazySingletonSyncMethod instance;
private LazySingletonSyncMethod() {
System.out.println(Thread.currentThread().getName() + " 创建实例");
}
// synchronized修饰静态方法,锁对象为 LazySingletonSyncMethod.class
public static synchronized LazySingletonSyncMethod getInstance() {
if (instance == null) {
instance = new LazySingletonSyncMethod();
}
return instance;
}
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(() -> {
LazySingletonSyncMethod.getInstance();
}).start();
}
// 只会有一个线程创建实例
}
}
性能瓶颈:当实例创建后,后续所有读操作仍需竞争锁。在JDK6之后经过锁优化(偏向锁、轻量级锁),性能有所提升,但仍无法与无锁方案媲美。
4. DCL双重检查锁定(重点分析volatile与JMM)
DCL(Double-Checked Locking)是懒加载单例的经典高性能实现,其正确性高度依赖于volatile关键字。
java
/**
* DCL双重检查锁定单例
*
* 核心知识点:
* 1. volatile的可见性与禁止指令重排语义
* 2. JMM(Java内存模型)中对象创建的三个步骤
* 3. 为什么必须用volatile修饰instance
*/
public class DCLSingleton {
/**
* volatile 关键作用:
* 1. 保证多线程间的可见性:一个线程修改了instance引用,其他线程立即可见
* 2. 禁止指令重排序:防止 instance = new DCLSingleton() 这个操作内部的
* "分配内存空间"、"初始化对象"、"将引用指向内存地址"三步发生重排序
*/
private static volatile DCLSingleton instance;
private DCLSingleton() {
System.out.println(Thread.currentThread().getName() + " DCL实例创建");
}
public static DCLSingleton getInstance() {
// 第一次检查:避免已创建实例后不必要的同步开销
if (instance == null) {
// 同步代码块,锁对象为 DCLSingleton.class
synchronized (DCLSingleton.class) {
// 第二次检查:防止多个线程同时通过第一次检查后,
// 在竞争锁的过程中实例已被其他线程创建
if (instance == null) {
/*
* 对象创建三步曲(无volatile时可能被重排序):
* 1. memory = allocate() // 分配内存空间
* 2. ctorInstance(memory) // 初始化对象
* 3. instance = memory // 将instance引用指向内存地址
*
* JIT编译器可能将步骤2和步骤3重排序为:
* 1 -> 3 -> 2,此时instance已经非null但对象尚未初始化完成。
* 另一个线程进入第一次if (instance == null)时发现非null,
* 直接返回一个未初始化完成的对象,导致不可预知的错误。
*
* volatile通过内存屏障(Memory Barrier)禁止这种重排序。
* - 在volatile写操作前插入StoreStore屏障,禁止前面的普通写与volatile写重排
* - 在volatile写操作后插入StoreLoad屏障,禁止后面的volatile读/写与当前volatile写重排
*/
instance = new DCLSingleton();
}
}
}
return instance;
}
public static void main(String[] args) {
for (int i = 0; i < 20; i++) {
new Thread(() -> DCLSingleton.getInstance()).start();
}
}
}
JMM规范深度解释 :在Java内存模型中,volatile字段的写操作happens-before于后续对该字段的读操作。这保证了当线程A完成instance的初始化后,线程B读取instance时能立即看到完整构造的对象。如果缺少volatile,线程B可能看到一个未完全构造的对象(部分字段为默认值),导致严重bug。该问题在JDK5之前确实存在,JDK5增强了JMM后,DCL配合volatile才成为可靠的方案。
5. 静态内部类Holder模式(分析类加载的懒加载与线程安全机制)
这是一种结合了饿汉式线程安全与懒汉式延迟加载的优雅方案,利用了JVM类加载的懒加载特性。
java
/**
* 静态内部类(Holder)单例模式
*
* 底层原理:
* 1. 类加载的懒加载时机:JVM规定,类在使用时才进行加载和初始化。
* SingletonHolder类只有在调用getInstance()方法访问其静态字段INSTANCE时
* 才会被加载和初始化,从而实现了延迟实例化。
* 2. 线程安全性保证:JVM在类的初始化阶段(执行<clinit>方法)会获取一个
* Class对象锁,保证多线程环境下类的静态初始化过程是线程安全的。
* 3. 无需synchronized,性能极佳,是目前最推荐的非枚举单例实现方式。
*/
public class HolderSingleton {
private HolderSingleton() {
System.out.println("HolderSingleton 实例化");
}
// 静态内部类,持有外部类单例
private static class SingletonHolder {
// 静态字段,在内部类被加载时初始化
private static final HolderSingleton INSTANCE = new HolderSingleton();
}
public static HolderSingleton getInstance() {
// 首次调用时触发 SingletonHolder 类的加载和初始化
return SingletonHolder.INSTANCE;
}
// 类加载触发时机测试方法
public static void otherStaticMethod() {
System.out.println("调用外部类的静态方法不会触发内部类加载");
}
public static void main(String[] args) {
System.out.println("--- 开始测试 ---");
HolderSingleton.otherStaticMethod(); // 不会触发内部类加载
System.out.println("--- 首次获取单例 ---");
HolderSingleton s1 = HolderSingleton.getInstance(); // 触发内部类加载
HolderSingleton s2 = HolderSingleton.getInstance();
System.out.println(s1 == s2);
}
}
类加载触发时机的深入解析 :JVM规范明确规定了类主动使用的七种场景,其中包括"读取或设置一个类的静态字段(被final修饰、已在编译期把结果放入常量池的静态字段除外)"。SingletonHolder.INSTANCE正是这样一种主动使用,因此会触发内部类的初始化。而外部类的静态方法otherStaticMethod()不会引用内部类,故不会导致其加载。这种懒加载机制实现了按需初始化,避免了饿汉式的资源浪费。
6. 枚举单例(《Effective Java》推荐的最佳实践)
枚举单例是Josh Bloch大力推崇的方式,它不仅能防止多线程并发问题,还对反射攻击和序列化攻击具有天然免疫力。
java
/**
* 枚举单例模式 - 最安全的单例实现
*
* 优势分析:
* 1. 线程安全:枚举实例的创建由JVM在类加载时完成,与饿汉式类似,天然线程安全
* 2. 防反射攻击:Constructor.newInstance()源码中会判断如果是枚举类型则抛出异常
* 3. 防序列化攻击:枚举的序列化机制只存储枚举名称,反序列化时通过valueOf方法获取
* 已有的枚举常量,不会创建新实例。
* 4. 代码简洁:无需私有构造器、getInstance方法等样板代码
*/
public enum EnumSingleton {
INSTANCE; // 唯一的枚举常量
// 可以添加属性和方法
private String configValue = "default";
public void doSomething() {
System.out.println("执行单例业务逻辑,实例hashCode: " + this.hashCode());
}
public String getConfigValue() {
return configValue;
}
public void setConfigValue(String configValue) {
this.configValue = configValue;
}
// 测试Main方法
public static void main(String[] args) {
EnumSingleton s1 = EnumSingleton.INSTANCE;
EnumSingleton s2 = EnumSingleton.INSTANCE;
s1.setConfigValue("updated");
System.out.println(s1 == s2); // true
System.out.println(s1.getConfigValue()); // updated
System.out.println(s2.getConfigValue()); // updated
s1.doSomething();
}
}
枚举单例的底层保障 :枚举类型在Java中本质上是一个继承自java.lang.Enum的final类,其所有常量均为public static final字段,类加载时初始化。JVM层面保证了枚举常量的唯一性。后续攻防章节将进一步追溯源码证明其防御机制。
二、攻防分析:反射与序列化破坏单例及终极防御
单例模式的"唯一实例"承诺在普通使用场景下是可靠的,但面对Java的反射机制与序列化机制,普通实现(饿汉、懒汉、DCL、Holder)均存在被破坏的风险。本章节通过PoC攻击代码演示破坏过程,并深入JDK源码解释防御原理。
1. 反射攻击:setAccessible破坏单例与防御
攻击演示:通过反射调用私有构造器,创建多个实例。
java
import java.lang.reflect.Constructor;
/**
* 反射攻击PoC:破坏除枚举外的所有单例实现
*/
public class ReflectionAttack {
public static void main(String[] args) throws Exception {
// 目标:攻击 DCLSingleton (同样适用于HolderSingleton、饿汉式等)
DCLSingleton instance1 = DCLSingleton.getInstance();
DCLSingleton instance2 = null;
// 1. 获取单例类的Class对象
Class<DCLSingleton> clazz = DCLSingleton.class;
// 2. 获取私有构造器
Constructor<DCLSingleton> constructor = clazz.getDeclaredConstructor();
// 3. 突破访问权限检查
constructor.setAccessible(true);
// 4. 通过反射创建新实例
instance2 = constructor.newInstance();
System.out.println("instance1 hashCode: " + instance1.hashCode());
System.out.println("instance2 hashCode: " + instance2.hashCode());
System.out.println("单例被破坏: " + (instance1 != instance2)); // true
}
}
防御方案:在私有构造器中添加标志位检测
java
/**
* 增强型DCL单例(防反射)
* 通过在构造器中检查实例是否已存在来防御反射攻击
*/
public class DCLSingletonReflectionProof {
private static volatile DCLSingletonReflectionProof instance;
// 标志位,记录实例是否已创建
private static volatile boolean isCreated = false;
private DCLSingletonReflectionProof() {
synchronized (DCLSingletonReflectionProof.class) {
if (isCreated) {
throw new RuntimeException("单例已被创建,禁止通过反射重复实例化");
}
isCreated = true;
System.out.println("DCLSingletonReflectionProof 实例化");
}
}
public static DCLSingletonReflectionProof getInstance() {
if (instance == null) {
synchronized (DCLSingletonReflectionProof.class) {
if (instance == null) {
instance = new DCLSingletonReflectionProof();
}
}
}
return instance;
}
}
枚举对反射攻击的天然免疫------追溯Constructor.newInstance源码
攻击枚举单例时会发现constructor.newInstance()抛出IllegalArgumentException: Cannot reflectively create enum objects。
我们追溯到JDK源码java.lang.reflect.Constructor的newInstance方法:
java
// JDK 8 源码片段
public T newInstance(Object ... initargs) throws InstantiationException, IllegalAccessException,
IllegalArgumentException, InvocationTargetException {
if (!override) {
if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
Class<?> caller = Reflection.getCallerClass();
checkAccess(caller, clazz, null, modifiers);
}
}
// 关键判断:如果是枚举类型,直接抛出异常
if ((clazz.getModifiers() & Modifier.ENUM) != 0)
throw new IllegalArgumentException("Cannot reflectively create enum objects");
ConstructorAccessor ca = constructorAccessor; // read volatile
if (ca == null) {
ca = acquireConstructorAccessor();
}
@SuppressWarnings("unchecked")
T inst = (T) ca.newInstance(initargs);
return inst;
}
从源码可见,JDK在反射创建实例时特意检查了Modifier.ENUM标志位,如果是枚举类则直接抛出异常,从而在语言层面彻底封堵了反射攻击的路径。
2. 序列化攻击:ObjectInputStream破坏单例与防御
攻击演示:通过序列化与反序列化创建新的实例。
java
import java.io.*;
/**
* 序列化攻击PoC:针对普通单例(非枚举)
*/
public class SerializationAttack {
public static void main(String[] args) throws Exception {
// 以 HolderSingleton 为例
HolderSingleton instance1 = HolderSingleton.getInstance();
HolderSingleton instance2 = null;
// 序列化到内存字节流
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(instance1);
oos.close();
// 反序列化
ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bais);
instance2 = (HolderSingleton) ois.readObject();
ois.close();
System.out.println("instance1 hashCode: " + instance1.hashCode());
System.out.println("instance2 hashCode: " + instance2.hashCode());
System.out.println("单例被破坏: " + (instance1 != instance2)); // true
}
}
防御原理:readResolve方法
Java序列化规范提供了一种回调机制:如果可序列化类定义了readResolve()方法,反序列化过程中会调用该方法,并将其返回的对象作为readObject()的最终结果。利用此特性,我们可以返回已存在的单例实例。
java
import java.io.Serializable;
/**
* 可序列化且防序列化攻击的单例(Holder模式版)
*/
public class HolderSingletonSerializable implements Serializable {
private static final long serialVersionUID = 1L;
private HolderSingletonSerializable() {
System.out.println("HolderSingletonSerializable 构造器调用");
}
private static class SingletonHolder {
private static final HolderSingletonSerializable INSTANCE = new HolderSingletonSerializable();
}
public static HolderSingletonSerializable getInstance() {
return SingletonHolder.INSTANCE;
}
/**
* 反序列化时JVM会自动调用此方法,返回的实例会替代反序列化生成的新对象
* 调用时机:ObjectInputStream在读取对象数据后,通过反射调用此方法
*/
private Object readResolve() {
System.out.println("readResolve 被调用,返回已有单例");
return SingletonHolder.INSTANCE;
}
}
枚举序列化的特殊处理机制------追溯ObjectInputStream.readEnum源码
枚举的序列化由JVM特殊处理,并不遵循普通对象的序列化规则。我们查看ObjectInputStream.readEnum()方法的核心逻辑(JDK 8源码):
java
// ObjectInputStream 中处理枚举反序列化的方法
private Enum<?> readEnum(boolean unshared) throws IOException {
// ... 读取枚举类型描述符 ...
String name = readString(false); // 读取枚举常量名称
Enum<?> result = null;
Class<?> cl = enumType;
if (cl != null) {
try {
@SuppressWarnings("unchecked")
// 通过 Enum.valueOf(Class, String) 获取已存在的枚举常量
Enum<?> en = Enum.valueOf((Class)cl, name);
result = en;
} catch (IllegalArgumentException ex) {
// ... 异常处理 ...
}
// ...
}
return result;
}
可见,枚举反序列化并非重新构造一个新对象,而是通过Enum.valueOf()方法根据名称查找当前JVM中已存在的枚举常量。由于枚举常量在类加载时已全部创建完毕,反序列化得到的始终是同一个实例。因此枚举单例对序列化攻击也是天然免疫的。
三、源码级应用分析:JDK与主流框架中的单例实践
1. JDK经典案例
(1) Runtime类 ------ 饿汉式单例的典范
java
// java.lang.Runtime 源码片段
public class Runtime {
private static final Runtime currentRuntime = new Runtime();
public static Runtime getRuntime() {
return currentRuntime;
}
private Runtime() {}
// ... 其他方法 ...
}
Runtime类代表Java应用程序的运行时环境,每个Java应用仅需一个Runtime实例来与JVM交互(如执行GC、获取内存信息等)。JDK采用标准的饿汉式实现,简单可靠。
(2) Collections.EMPTY_LIST ------ 不可变单例容器
java
// java.util.Collections 源码片段
public class Collections {
@SuppressWarnings("rawtypes")
public static final List EMPTY_LIST = new EmptyList<>();
// 返回类型安全的空列表单例
@SuppressWarnings("unchecked")
public static final <T> List<T> emptyList() {
return (List<T>) EMPTY_LIST;
}
// 内部静态类,不可变空列表实现
private static class EmptyList<E> extends AbstractList<E> implements RandomAccess, Serializable {
// ... 所有修改操作均抛出UnsupportedOperationException ...
}
}
此模式属于享元模式与单例模式的结合。由于空列表是无状态的不可变对象,全局共享一个实例既能节省内存,又无线程安全问题。
(3) Console类 ------ 懒汉式变体(双重检查锁风格)
java
// java.io.Console 源码片段
public final class Console implements Flushable {
private static volatile Console cons;
private Console() {}
public static Console console() {
if (cons == null) {
synchronized (System.class) { // 注意锁对象是System.class
if (cons == null) {
cons = SharedSecrets.getJavaIOAccess().console();
}
}
}
return cons;
}
}
Console类使用了DCL变体,锁对象选择了System.class,因为控制台与系统输入输出紧密相关。这体现了单例与系统资源绑定的设计思路。
2. Spring源码:DefaultSingletonBeanRegistry三级缓存机制
Spring IoC容器中,默认作用域的Bean均为单例。其单例管理的核心位于DefaultSingletonBeanRegistry类,通过三级缓存巧妙解决了循环依赖问题。
核心缓存定义:
java
// org.springframework.beans.factory.support.DefaultSingletonBeanRegistry
public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements SingletonBeanRegistry {
/** 一级缓存:存放完全初始化好的单例Bean,即成品 */
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);
/** 二级缓存:存放早期暴露的单例Bean(未完成属性填充和初始化),用于解决循环依赖 */
private final Map<String, Object> earlySingletonObjects = new ConcurrentHashMap<>(16);
/** 三级缓存:存放可以生成Bean的工厂对象(ObjectFactory),用于延迟生成代理对象 */
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);
// 正在创建中的Bean名称集合
private final Set<String> singletonsCurrentlyInCreation = Collections.newSetFromMap(new ConcurrentHashMap<>(16));
}
getSingleton方法源码分析(解决循环依赖流程):
java
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
// 步骤1:从一级缓存获取
Object singletonObject = this.singletonObjects.get(beanName);
// 步骤2:如果一级缓存没有,且该Bean正在创建中
if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
// 步骤3:从二级缓存获取早期引用
singletonObject = this.earlySingletonObjects.get(beanName);
if (singletonObject == null && allowEarlyReference) {
synchronized (this.singletonObjects) {
// Double Check
singletonObject = this.singletonObjects.get(beanName);
if (singletonObject == null) {
singletonObject = this.earlySingletonObjects.get(beanName);
if (singletonObject == null) {
// 步骤4:从三级缓存获取工厂,创建早期Bean引用
ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
if (singletonFactory != null) {
singletonObject = singletonFactory.getObject();
// 步骤5:升级到二级缓存,并从三级缓存移除
this.earlySingletonObjects.put(beanName, singletonObject);
this.singletonFactories.remove(beanName);
}
}
}
}
}
}
return singletonObject;
}
三级缓存解决循环依赖的经典流程:
- A创建中 :实例化A(通过构造器),将A的
ObjectFactory放入三级缓存。 - A填充属性B:发现依赖B,开始创建B。
- B创建中 :实例化B,将B的
ObjectFactory放入三级缓存。 - B填充属性A :从缓存中获取A。通过
getSingleton从三级缓存获取A的工厂,产生A的早期引用(可能为代理),放入二级缓存,并注入给B。 - B完成初始化:B成为完整Bean,放入一级缓存。
- A完成初始化:A拿到完整的B,继续属性填充及初始化,最终放入一级缓存。
注:三级缓存时序图见第七章节Mermaid图表。
3. MyBatis源码:ErrorContext基于ThreadLocal的线程级单例
MyBatis的ErrorContext类用于记录当前线程在执行SQL时的上下文错误信息,每个线程应有自己独立的ErrorContext实例,避免多线程间信息串扰,这属于线程级单例模式。
java
// org.apache.ibatis.executor.ErrorContext 源码简化
public class ErrorContext {
// ThreadLocal确保每个线程拥有独立的ErrorContext实例
private static final ThreadLocal<ErrorContext> LOCAL = ThreadLocal.withInitial(ErrorContext::new);
private ErrorContext() {}
public static ErrorContext instance() {
return LOCAL.get();
}
// 存储当前线程的错误信息
private String resource;
private String activity;
private String object;
// ... getter/setter ...
}
这种模式在"单例"的范围定义上做了延伸:单例并非一定是整个JVM唯一,也可以限定在线程范围内唯一 。类似的设计在Spring的RequestContextHolder、TransactionSynchronizationManager中也有体现。
4. Logback源码:LoggerFactory的ILoggerFactory绑定与单例管理
Logback是SLF4J的原生实现,其核心LoggerContext作为ILoggerFactory的实现,在整个应用中应当只有一个实例。
java
// ch.qos.logback.classic.LoggerContext 绑定过程简化
public final class LoggerFactory {
static final String STATIC_LOGGER_BINDER_PATH = "org/slf4j/impl/StaticLoggerBinder.class";
public static ILoggerFactory getILoggerFactory() {
// ...
// 通过StaticLoggerBinder获取单例的LoggerContext
StaticLoggerBinder binder = StaticLoggerBinder.getSingleton();
return binder.getLoggerFactory();
}
}
// ch.qos.logback.classic.spi.LogbackServiceProvider (或 StaticLoggerBinder)
public class StaticLoggerBinder {
private static StaticLoggerBinder SINGLETON = new StaticLoggerBinder();
public static StaticLoggerBinder getSingleton() {
return SINGLETON;
}
private LoggerContext defaultLoggerContext = new LoggerContext();
public ILoggerFactory getLoggerFactory() {
return defaultLoggerContext;
}
}
LoggerContext内部维护了所有Logger实例的缓存(通过ConcurrentHashMap),其本身作为应用日志系统的根上下文,必须是单例的,以保证配置的统一和资源的集中管理。
四、分布式环境下的单例:从JVM唯一到集群唯一
单例模式的传统定义局限于单个JVM进程。当应用部署在多台服务器构成集群时,如何保证某个逻辑对象在整个集群中只有一个实例?这需要借助分布式协调服务。
1. 类加载器隔离导致Tomcat多WebApp场景下静态单例失效问题
问题场景 :Tomcat容器中部署了多个Web应用(WebAppA和WebAppB),两个应用都依赖了同一个包含单例模式的工具Jar包(如一个全局ID生成器)。由于Tomcat为每个WebApp创建独立的WebappClassLoader,同一个类会被不同的类加载器各加载一次,从而产生两个独立的Class对象和两个静态变量副本,导致单例失效。
解决方案:
- 将Jar包置于Tomcat的
lib目录:由Common类加载器加载,所有WebApp共享同一个类定义。 - 使用容器管理的单例:如将单例注册为JNDI资源。
- 采用分布式单例方案(见下文),将唯一性保证交给外部协调服务,避免依赖类加载器层级。
2. 基于Redis分布式锁实现集群单例
利用Redis的SETNX(SET if Not eXists)命令实现互斥,确保在某一时刻只有一个服务节点能获取到"执行权"。
java
import redis.clients.jedis.Jedis;
import redis.clients.jedis.params.SetParams;
/**
* 基于Redis的分布式单例协调器
* 模拟场景:集群中只有一个节点可以执行定时任务
*/
public class RedisDistributedSingleton {
private static final String LOCK_KEY = "cluster:singleton:task";
private static final String LOCK_VALUE = "instance-001"; // 建议使用UUID或主机标识
private static final int LOCK_EXPIRE = 30; // 锁过期时间,防止死锁
private Jedis jedis;
public RedisDistributedSingleton() {
// 初始化Redis连接
jedis = new Jedis("localhost", 6379);
}
/**
* 尝试获取"单例执行权"
*/
public boolean tryAcquireLeadership() {
SetParams params = new SetParams().nx().ex(LOCK_EXPIRE);
String result = jedis.set(LOCK_KEY, LOCK_VALUE, params);
return "OK".equals(result);
}
/**
* 续期锁,适用于长任务
*/
public boolean renewLeadership() {
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then " +
"return redis.call('expire', KEYS[1], ARGV[2]) else return 0 end";
Object result = jedis.eval(script, 1, LOCK_KEY, LOCK_VALUE, String.valueOf(LOCK_EXPIRE));
return "1".equals(result.toString());
}
/**
* 释放执行权(仅在持有时释放)
*/
public void releaseLeadership() {
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then " +
"return redis.call('del', KEYS[1]) else return 0 end";
jedis.eval(script, 1, LOCK_KEY, LOCK_VALUE);
}
public static void main(String[] args) throws InterruptedException {
RedisDistributedSingleton singleton = new RedisDistributedSingleton();
// 模拟多个节点竞争
if (singleton.tryAcquireLeadership()) {
try {
System.out.println("获取集群单例执行权,开始执行任务...");
// 模拟长任务
for (int i = 0; i < 60; i++) {
Thread.sleep(1000);
// 每10秒续期一次
if (i % 10 == 0) {
singleton.renewLeadership();
}
}
} finally {
singleton.releaseLeadership();
System.out.println("任务执行完毕,释放执行权");
}
} else {
System.out.println("未获取到执行权,本节点作为备用");
}
}
}
设计思路 :此模式并非严格的"单例对象",而是通过分布式锁实现了单例行为的保障------保证某个任务在同一时间只有一个节点执行。适用于定时任务调度、资源清理等场景。
3. 基于ZooKeeper的Curator LeaderLatch实现分布式单例
Apache Curator提供了高级API LeaderLatch,封装了基于ZK临时顺序节点实现的Leader选举机制。
java
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.framework.recipes.leader.LeaderLatch;
import org.apache.curator.framework.recipes.leader.LeaderLatchListener;
import org.apache.curator.retry.ExponentialBackoffRetry;
import java.util.concurrent.TimeUnit;
/**
* 基于Curator LeaderLatch的分布式单例
* 只有被选为Leader的节点才能执行业务逻辑
*/
public class ZkDistributedSingleton {
private static final String ZK_CONNECT_STRING = "localhost:2181";
private static final String LEADER_PATH = "/cluster/singleton/leader";
private CuratorFramework client;
private LeaderLatch leaderLatch;
public ZkDistributedSingleton(String instanceId) {
// 创建Curator客户端
client = CuratorFrameworkFactory.builder()
.connectString(ZK_CONNECT_STRING)
.retryPolicy(new ExponentialBackoffRetry(1000, 3))
.build();
client.start();
// 创建LeaderLatch
leaderLatch = new LeaderLatch(client, LEADER_PATH, instanceId);
leaderLatch.addListener(new LeaderLatchListener() {
@Override
public void isLeader() {
System.out.println(instanceId + " 成为Leader,开始执行单例业务");
executeSingletonTask();
}
@Override
public void notLeader() {
System.out.println(instanceId + " 失去Leader身份,停止单例业务");
}
});
}
public void start() throws Exception {
leaderLatch.start();
}
public void close() throws Exception {
leaderLatch.close();
client.close();
}
private void executeSingletonTask() {
// 执行只有Leader才能做的操作,例如:清理过期数据、推送全局配置等
new Thread(() -> {
while (leaderLatch.hasLeadership()) {
try {
System.out.println("Leader节点执行业务...");
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}).start();
}
public static void main(String[] args) throws Exception {
// 模拟集群中三个节点
ZkDistributedSingleton node1 = new ZkDistributedSingleton("node-1");
ZkDistributedSingleton node2 = new ZkDistributedSingleton("node-2");
ZkDistributedSingleton node3 = new ZkDistributedSingleton("node-3");
node1.start();
node2.start();
node3.start();
// 让程序运行一段时间观察Leader切换
TimeUnit.MINUTES.sleep(2);
node1.close();
node2.close();
node3.close();
}
}
LeaderLatch原理 :每个参与者都在ZK的指定路径下创建一个临时顺序节点,序号最小的节点成为Leader。当Leader节点会话断开或主动释放,临时节点被删除,剩余节点中序号最小的自动当选新Leader。这种机制保证了集群中始终有且仅有一个Leader,从而实现分布式单例语义。
五、适用场景分析:何处用单例,何处需谨慎
1. 资源池化场景:数据库连接池、线程池为何需要单例
技术理由:
- 资源昂贵且可共享:数据库连接创建开销大(TCP握手、认证),池化复用可显著提升性能。单例连接池确保全局唯一入口,避免多处独立创建池导致连接数失控。
- 统一配置与管理:连接池参数(最大连接数、超时时间)需在应用级别保持一致,多实例易造成配置冲突和资源浪费。
- 生命周期与JVM绑定:连接池通常随应用启动创建,随应用关闭销毁,与JVM进程生命周期一致,适合单例管理。
2. 全局配置管理:应用配置对象、系统参数对象的单例设计
技术理由:
- 数据一致性:配置信息(如数据库URL、第三方API密钥)在运行期间通常是只读的,单例可保证所有模块获取到一致的配置视图。
- 减少重复加载开销:从文件、环境变量或配置中心加载配置可能涉及IO或网络请求,单例模式结合懒加载可将开销降至最低。
- 支持动态刷新 :单例配置对象可配合监听器机制,实现配置热更新而不影响客户端引用(例如
@ConfigurationProperties在Spring Cloud Config中的运用)。
3. 无状态工具类:如日期格式化、加密解密工具类的单例化考量
技术理由:
- 无状态所以线程安全:工具类方法不依赖成员变量,天然线程安全。单例化可减少对象创建开销。
- 反面警示------SimpleDateFormat并非无状态 :
SimpleDateFormat内部持有Calendar对象,多线程共享会导致日期解析错乱。此类有状态工具类严禁使用静态单例 ,应使用ThreadLocal或每次新建实例。
4. 硬件接口访问:打印机、串口等独占资源场景
技术理由:
- 物理独占性 :打印机、串口等硬件设备同一时刻只能被一个任务操作。单例对象作为硬件访问的逻辑代理,可内置同步锁或队列机制来串行化访问请求,避免并发操作导致设备状态混乱或数据损坏。
- 生命周期统一管理:在单例对象中集中打开/关闭硬件连接,简化资源管理逻辑。
5. Spring Bean作用域:singleton作用域的适用边界与注意事项
适用边界:
- 无状态Service、Controller、Repository:默认单例是正确且高效的选择。
- 共享的只读配置Bean。
- 线程安全的工具组件。
注意事项:
- 避免在单例Bean中定义可变实例变量 :
@Controller中持有请求相关的成员变量,会导致线程安全问题(Spring本身不保证单例Bean的线程安全,需要开发者自己保证)。 - 注意作用域依赖 :单例Bean中注入
prototype作用域的Bean,后者只会被注入一次,无法实现每次获取都得到新实例。需使用@Lookup或ApplicationContext.getBean()。
6. 不适用场景警示:哪些场景应避免使用单例
- 携带可变状态的对象 :如上述
SimpleDateFormat,以及含有用户会话数据的对象。此类对象应使用prototype或request/session作用域。 - 高并发下的瓶颈点 :若单例方法内部有重量级
synchronized锁,会成为系统并发度的天花板。例如早期Hashtable的单例运用(现已由ConcurrentHashMap替代)。 - 需频繁创建销毁的轻量级对象:如DTO、VO,单例化反而增加状态管理的复杂度。
- 与特定上下文绑定的对象 :如
HttpServletRequest、HttpServletResponse,其生命周期由容器管理,强行单例化无意义。
六、面试题精选与专家级解答
以下精选10道单例模式相关的高频Java专家面试题,并提供有技术深度的参考答案。
1. 为什么DCL需要volatile?从JMM角度解释
参考答案 : DCL中instance = new Singleton()这一行代码在JVM层面并非原子操作,可分解为三个步骤:
- 分配内存空间。
- 执行构造方法初始化对象。
- 将
instance引用指向分配的内存地址。
JMM允许编译器和处理器对指令进行重排序 以提高执行效率。上述步骤可能被重排为1→3→2 的执行顺序。当线程A执行完1和3后,instance已经不为null,但对象尚未初始化完成(字段均为默认值)。此时若线程B执行第一次if (instance == null)检查,发现非null,直接返回未初始化完成的对象,后续调用其方法或访问字段将引发不可预知的错误(如NPE或数据不一致)。
volatile关键字通过以下机制禁止这种重排序:
- 内存屏障 :在
volatile写操作(如对instance的赋值)前后插入StoreStore屏障和StoreLoad屏障,防止前面的写操作与后面的volatile写操作重排,也防止后面的读/写操作与volatile写重排。 - happens-before规则 :对
volatile变量的写操作happens-before 后续对该变量的读操作,保证线程B读取instance时,能看到线程A写入instance之前的所有操作结果,即看到的是一个完整构造的对象。
因此,缺少volatile的DCL在JDK5之前(及之后未使用volatile时)存在严重的安全隐患。
2. 枚举单例是如何防止反射和序列化攻击的?
参考答案 : 防反射攻击 :JDK在java.lang.reflect.Constructor.newInstance()方法中有显式判断:
java
if ((clazz.getModifiers() & Modifier.ENUM) != 0)
throw new IllegalArgumentException("Cannot reflectively create enum objects");
当通过反射尝试创建枚举实例时,JVM直接抛出异常,从根本上禁止了枚举的反射实例化。
防序列化攻击 :枚举的序列化与反序列化有特殊处理。序列化时,仅将枚举常量的名称 写入流中,不保存任何字段状态。反序列化时,ObjectInputStream.readEnum()方法根据读取到的名称,调用Enum.valueOf(Class<T> enumType, String name)获取对应的枚举常量。由于枚举常量在类加载时已全部实例化完成,valueOf返回的是JVM中已存在的同一实例,因此不会产生新对象。这种机制保证了枚举在序列化反序列化过程中的单例性。
3. 单例模式在集群环境下如何保证唯一性?
参考答案 : 集群环境下,JVM进程相互隔离,内存级单例无法跨进程生效。保证集群唯一性需借助分布式协调中间件:
- 基于数据库唯一约束:通过插入一条记录到带有唯一索引的表来抢占"单例执行权",任务结束后删除记录。缺点是不够实时,需处理锁超时。
- 基于Redis分布式锁 :利用
SETNX命令实现互斥,可设置过期时间防止死锁。适合轻量级的任务互斥。 - 基于ZooKeeper :利用ZK的临时顺序节点和Watcher机制实现Leader选举。Curator框架的
LeaderLatch封装了此模式,当Leader宕机时自动触发重新选举,保证集群中始终有且仅有一个Leader节点。 - 基于配置中心下发指令:如使用Nacos、Apollo下发一个开关变量,只有特定IP的节点执行任务。
4. Spring的单例Bean是线程安全的吗?为什么?
参考答案 : Spring容器本身并不保证单例Bean的线程安全性。Spring只是管理Bean的创建和装配,Bean的线程安全取决于其自身的状态管理方式。
- 如果单例Bean是无状态 的(例如典型的
@Service、@Repository、@Controller,没有可变的成员变量),那么它自然是线程安全的。 - 如果单例Bean持有可变状态 (如成员变量),并且有多个线程并发修改该状态,则会出现线程安全问题。此时需要开发者通过
sychronized、Lock或使用ThreadLocal将状态线程私有化来保证安全。 - 对于有状态的Bean,Spring提供了
prototype、request、session等作用域来为每次访问创建新实例,从根本上避免共享状态问题。
5. 单例模式与静态工具类在设计上的本质区别是什么?
参考答案:
- 面向对象设计 :单例模式是对象,可以实现接口、继承类,具备多态特性。静态工具类仅是一组静态方法的集合,无法实现接口或继承,是面向过程的编码风格。
- 延迟加载与资源管理:单例模式可控制实例化时机(懒加载),可以在构造器中执行资源初始化(如建立连接),并能在销毁时释放资源。静态类在类加载时初始化,生命周期完全由JVM类加载器控制,缺乏灵活的初始化和销毁机制。
- 状态保持:单例可以持有状态(尽管通常不推荐,但可以),静态类虽然也有静态变量,但缺乏封装性。
- 可测试性:单例由于是对象,可以通过依赖注入、Mock等方式替换或模拟,方便单元测试。静态工具类的方法调用是硬编码的,难以Mock。
6. 如何优雅地实现可销毁的单例对象?
参考答案: 标准的单例模式缺少显式的销毁接口。要实现可销毁的单例,可以考虑以下方案:
- 提供
destroy()方法并清空引用 :在单例类中添加public static synchronized void destroy()方法,内部将instance置为null,并执行资源清理。需要注意并发安全问题,在destroy()和getInstance()之间需同步协调。 - 使用JVM Shutdown Hook :通过
Runtime.getRuntime().addShutdownHook(Thread)注册钩子,在JVM关闭前执行清理逻辑。 - 实现
AutoCloseable接口 :让单例类实现AutoCloseable,在try-with-resources块中使用,但需注意单例获取方式需适配。 - 借助容器管理 :如在Spring中,单例Bean会随容器关闭而销毁,可声明
@PreDestroy方法进行资源释放。
7. 单例模式可能带来哪些代码坏味道?如何重构?
参考答案 : 代码坏味道:
- 隐式全局状态:单例本质是全局变量,导致模块间产生隐式耦合,代码难以理解和测试。
- 违反单一职责原则:单例类往往不仅管理自身实例,还承担业务逻辑,职责不清。
- 阻碍单元测试:单例的全局状态会在多个测试用例间串扰,必须手动重置状态。
- 强依赖导致难以扩展 :直接调用
Singleton.getInstance()形成了硬编码依赖,替换实现困难。
重构策略:
- 依赖注入(DI):通过构造器或Setter将单例作为依赖注入,由容器(如Spring)管理其生命周期。客户端不感知单例,只依赖接口。
- 引入工厂模式:将单例获取逻辑封装在工厂中,降低直接耦合。
- 使用枚举替代:枚举单例更简洁、安全,且易于测试(可通过扩展性受限于语言特性)。
8. 类加载器如何破坏单例?如何防御?
参考答案 : 破坏机制 :JVM中,类的唯一性由类加载器 和类全限定名 共同决定。如果同一个单例类被不同的类加载器加载(例如Tomcat的多个WebApp),JVM中会存在多个Class对象,它们的静态变量是隔离的,从而产生多个"单例"实例。
防御方案:
- 统一类加载器 :将单例所在的Jar包置于更高层级的类加载器路径下(如Tomcat的
lib目录、Java的ext目录),确保所有WebApp使用同一个类定义。 - 使用容器JNDI资源:通过JNDI绑定单例对象,由容器管理唯一性。
- 显式指定类加载器:在获取单例时,强制使用某个特定类加载器加载类(需谨慎,可能引发更多类加载问题)。
- 设计层面规避:不依赖静态单例,改用分布式单例或Spring容器单例Bean。
9. 饿汉式与懒汉式在性能、故障排查角度的取舍考量
参考答案:
| 维度 | 饿汉式 | 懒汉式(线程安全版如Holder) |
|---|---|---|
| 启动性能 | 类加载时立即初始化,若单例构造耗时,会延长应用启动时间 | 延迟到首次调用,启动快 |
| 运行时性能 | 无任何同步开销,获取速度快 | 首次调用有初始化开销,后续无同步开销(Holder)或需锁(方法同步) |
| 故障排查 | 启动时抛异常可立即发现,Fail-Fast | 运行时首次调用时才暴露初始化问题,排查相对被动 |
| 内存占用 | 可能提前占用内存,若从未使用则浪费 | 按需占用,更节约内存 |
| 实现复杂度 | 极简 | 较复杂(需考虑并发与指令重排) |
专家建议 :对于初始化快速、确定会被使用的单例,优先饿汉式 ;对于初始化成本高、可能不会被使用的单例,优先Holder模式 ;需要传递上下文或依赖外部资源的单例,使用枚举单例。
10. 除了枚举,还有哪些手段可以彻底防御反射攻击?
参考答案:
- 构造器内标志位检查(如前文所述) :在私有构造器中通过静态布尔标志位判断,若已创建则抛出异常。但此方法仍能被反射绕过 (先通过反射修改标志位为
false再创建)。 - 利用
SecurityManager:配置Java安全策略,禁止setAccessible(true)操作。但这会限制整个应用的反射能力,较为粗暴。 - 使用
Unsafe类破坏反射能力 :某些极端手段可通过Unsafe修改Constructor的modifiers或override字段状态,但不可移植,且JDK升级可能失效。 - 终极手段仍是枚举 :因为枚举的防御逻辑在JDK的
Constructor.newInstance()源码中,是语言层面的硬性规定,无法被任何Java代码绕过(除非修改JDK源码或使用非常规的字节码增强绕过反射检查)。
已为您在第七章节各Mermaid图表下方补充详细文字说明,便于读者深入理解流程图与时序图的每个步骤。以下是更新后的图表部分内容,您可直接替换原文相应章节。
七、图表要求:Mermaid流程图与时序图(附详细文字说明)
1. DCL双重检查锁定的执行流程图
(volatile 写屏障禁止重排序)"] G --> Z classDef default fill:#f9f9f9,stroke:#333,stroke-width:1px; classDef condition fill:#fff4e6,stroke:#ffa500,stroke-width:1.5px; classDef important fill:#ffebee,stroke:#f44336,stroke-width:2px; class B,D condition; class G important;
流程文字说明:
-
首次检查(instance == null?) :
线程进入
getInstance()后先执行无锁的null检查。若实例已存在,则直接返回,这是性能优化的关键------避免已初始化后的每次调用都进入同步块争抢锁,将高并发下的锁竞争降至最低。 -
获取类锁(synchronized块) :
仅当首次检查发现实例为
null时,线程才尝试获取Singleton.class的监视器锁。多个线程可能同时通过首次检查,但在锁竞争时只有一个能进入同步块,其余阻塞等待。 -
二次检查(再次检查 instance == null?) :
进入同步块的线程必须再次检查 实例是否仍为
null。原因在于:当前线程在等待锁期间,可能已有其他线程完成了实例创建并释放了锁。若不做二次检查,将导致重复创建实例,破坏单例唯一性。 -
对象创建三步曲与volatile的屏障作用:
- 分配内存:JVM为对象在堆上分配空间。
- 初始化对象:执行构造方法,为字段赋初始值。
- 引用指向 :将
instance变量指向分配的内存地址。
若无volatile修饰,步骤②和③可能被编译器或CPU指令重排 为①→③→②。此时若另一线程进入首次检查,将看到instance非null而直接返回一个未初始化完成的对象 (字段为默认值),引发NPE或逻辑错误。
volatile在步骤③前后插入StoreStore屏障 与StoreLoad屏障 ,强制禁止这种重排序,保证任何线程读取instance时,对象已完全构造完毕。
2. Spring三级缓存解决循环依赖的时序图
时序图文字说明:
-
触发A的创建 :
客户端请求
getBean("A"),Spring容器发现一级缓存中没有A,开始创建流程。 -
A的实例化与早期工厂暴露 :
Spring先调用A的构造器完成实例化 (此时属性尚未填充)。随后,将A的
ObjectFactory(一个能生成A早期引用的lambda表达式)放入三级缓存 。这一步是解决循环依赖的前置条件。 -
填充A的属性,触发B的创建 :
容器开始为A填充属性,发现A依赖B。于是递归调用
getBean("B")。由于B也不存在于任何缓存,容器同样实例化B并将B的ObjectFactory放入三级缓存。 -
B依赖A,从缓存获取A的早期引用 :
填充B时发现B依赖A,再次调用
getBean("A")。这次查找顺序为:一级缓存(无)→ 二级缓存(无)→ 三级缓存 。三级缓存中找到了A的ObjectFactory,调用其getObject()方法获得A的早期引用(若A需要AOP代理,此处会生成代理对象)。 -
缓存升级 :
Spring将获取到的A早期引用放入二级缓存,并从三级缓存中移除A的工厂。此举防止重复创建代理对象,保证后续获取的都是同一个早期引用。
-
B完成初始化 :
B拿到A的早期引用后,完成自身的属性填充与初始化,成为完整Bean ,并被放入一级缓存(同时移除二、三级缓存中的B)。
-
A完成初始化 :
递归返回后,A获得完整的B实例,继续完成自身的属性填充与初始化。最终A也成为完整Bean,放入一级缓存。
-
关键机制总结:
- 一级缓存存"成品",供外部获取。
- 二级缓存存"半成品早期引用",解决循环依赖。
- 三级缓存存"工厂",延迟生成代理对象,确保AOP在循环依赖场景下仍正确工作。
3. 序列化反序列化中readResolve调用时序图
时序图文字说明:
-
构造新对象(tempObj) :
ObjectInputStream从字节流中读取类描述信息,绕过构造器 直接在堆上重建一个新对象tempObj。这是序列化破坏单例的根本原因------即使构造器私有,反序列化也能创建新实例。 -
填充字段数据 :
OIS根据流中的持久化数据,通过反射将字段值恢复到tempObj中。此时tempObj在内存层面已是单例类的另一个"完整"实例。 -
检查readResolve方法 :
OIS通过反射检查该类是否定义了private Object readResolve()方法。这是Java序列化规范提供的回调钩子。 -
分支一:定义了readResolve
OIS调用tempObj.readResolve()。- 在该方法中,开发者编写的逻辑返回了真正的单例实例 (如
SingletonHolder.INSTANCE)。 OIS收到返回值后,丢弃掉刚刚构建的tempObj,将readResolve的返回值作为readObject()的最终结果返回给调用方。- 由于
tempObj不再被引用,稍后会被GC回收,从而保证了单例的唯一性。
-
分支二:未定义readResolve
OIS直接将tempObj作为反序列化结果返回。此时客户端获得的是一个全新的伪造实例,单例模式被彻底破坏。
-
枚举的特殊性 :
对于枚举类型,
ObjectInputStream不会执行上述流程,而是走readEnum()分支,通过名称查找已存在的枚举常量并返回,从根本上杜绝了新实例的产生。
八、结语
单例模式作为设计模式中的"Hello World",其简洁的外表下蕴藏着Java语言底层机制的众多精华------从类加载器的双亲委派模型到JMM的happens-before原则,从synchronized锁升级过程到序列化协议的回调钩子。本文通过六种实现方式的代码级剖析、反射与序列化的攻防实战、主流框架的源码探索以及分布式场景的扩展思考,全方位展现了单例模式在单机与集群环境中的最佳实践与潜在陷阱。
理解单例模式不仅是为了应对面试中的刁钻提问,更是为了在系统设计时能精准权衡资源利用、并发性能与代码可维护性。当您下次面对一个"只需一个实例"的需求时,希望本文能帮助您做出更具专业深度的技术决策。