openJiuwen agent-core-java 中的 SingletonSupport:线程安全单例创建与重置的通用基础设施
openJiuwen agent-core-java 中的 SingletonSupport:线程安全单例创建与重置的通用基础设施
导读
SingletonSupport 是 openJiuwen agent-core-java(com.openjiuwen.core.common.utils 包)提供的一个泛型抽象基类,为框架内各类服务组件提供"每个具体类仅有一个实例"的线程安全单例创建与重置能力。它的典型应用是 HttpSessionManager 这类全局共享的底层客户端管理器——通过它获取进程内唯一的 HTTP 会话管理器,避免在 Agent 运行、工具调用与 LLM 客户端之间重复创建昂贵资源。阅读本文后,你将掌握 SingletonSupport 的完整 API、双重检查锁(DCL)实现原理、reset 在测试中的用法,以及如何在自有服务类中复用它。
类概览与设计定位
类的声明与包位置
SingletonSupport 位于工具包 com.openjiuwen.core.common.utils 下,源码文件为 SingletonSupport.java,自版本 0.1.7 起提供:
public abstract class SingletonSupport<T>
它被声明为 abstract,意味着不能直接实例化,而是作为基类被具体服务类继承。从源码注释可以看到其设计来源:它是 Python 端 Singleton 元类(metaclass)的 Java 等价物,目标是保证"每个具体类恰好只有一个实例"(exactly one instance per concrete class)。
在 utils.README.md 的核心类型表中,SingletonSupport 被定位为"为 Java 服务类型提供线程安全单例支持",与 DictUtils(嵌套结构处理)、HashUtil(SHA-256 哈希键)、IpUtils(本机 IPv4 探测)、MessageUtils(消息上下文写入)、SchemaUtils(Schema 校验)并列,属于框架级通用工具。
泛型参数 T 的含义
SingletonSupport<T> 中的 T 代表单例实例的类型。基类本身不保存任何实例状态,仅承载静态的实例缓存与获取逻辑。典型写法是让具体服务类继承 SingletonSupport<MyService> 并将自己作为泛型实参传入(见下文"如何在自有类中使用"),从而获得类型安全的 getInstance() 返回。
字段:按具体类缓存的 INSTANCES
| 字段 | 类型 | 说明 |
|---|---|---|
INSTANCES |
ConcurrentHashMap<Class<?>, Object> |
按具体类缓存单例对象 |
该字段是 private static final,在类加载时初始化:
private static final ConcurrentHashMap<Class<?>, Object> INSTANCES = new ConcurrentHashMap<>();
选择 ConcurrentHashMap 而非普通 HashMap,是为了让外层"无锁快速路径"(INSTANCES.get(clazz))在并发读取场景下安全可见:当某个线程完成初始化并 put 之后,其他线程可以无阻塞地命中缓存。这也是整个类线程安全设计的第一层保障。
方法:getInstance 与 reset
| 签名 | 说明 |
|---|---|
public static <T> T getInstance(Class<T> clazz, java.util.function.Supplier<T> factory) |
通过双重检查锁获取或创建指定类的单例实例 |
public static void reset(Class<?> clazz) |
移除指定类的已缓存实例,主要用于测试 |
getInstance:双重检查锁的完整流程
源码实现(SingletonSupport.java)如下:
@SuppressWarnings("unchecked")
public static <T> T getInstance(Class<T> clazz, java.util.function.Supplier<T> factory) {
Object instance = INSTANCES.get(clazz);
if (instance == null) {
synchronized (SingletonSupport.class) {
instance = INSTANCES.get(clazz);
if (instance == null) {
instance = factory.get();
INSTANCES.put(clazz, instance);
}
}
}
return (T) instance;
}
其执行流程分为三个阶段:
- 第一次检查(无锁):先通过
INSTANCES.get(clazz)快速读取。绝大多数并发场景下实例已经存在,直接命中并返回,无需进入同步块,这也是ConcurrentHashMap在高并发读多写少场景下的核心价值。 - 加锁后的第二次检查:仅在缓存未命中时才进入
synchronized (SingletonSupport.class)临界区,并在临界区内再次读取缓存。这一步防止两个线程同时通过第一次检查后重复创建实例——即使两个线程都发现instance == null,也只有一个线程能持有锁完成创建,另一个线程会在进入临界区后看到已被放入的实例。 - 创建与缓存:由调用方传入的
factory(Supplier<T>)负责实际的对象构建,随后INSTANCES.put(clazz, instance)将实例按类缓存。
关于锁对象,需要注意它锁定的是 SingletonSupport.class 这个全局类对象,而非某个具体子类的 Class 对象。这意味着所有继承者共享同一把锁,锁粒度较粗但实现简单可靠;由于单例创建通常是低频操作(一次性初始化),这样的设计在框架场景下完全够用。
泛型与类型安全
方法签名 public static <T> T getInstance(Class<T> clazz, ...) 将实例类型与传入的 Class<T> 绑定。虽然内部缓存值是 Object(因为 ConcurrentHashMap<Class<?>, Object> 无法表达"每个键对应其自身类型的值"),返回时通过 @SuppressWarnings("unchecked") 完成向下转型。调用方只要保证 clazz 与 factory 创建的类型一致,即可获得类型安全的返回值。
reset:测试专用的重置入口
public static void reset(Class<?> clazz) {
INSTANCES.remove(clazz);
}
reset 通过 ConcurrentHashMap.remove(clazz) 移除指定类的已缓存实例。它被显式定位为"primarily for testing"——在单元测试中,若某个单例携带状态(如缓存、连接、计数器),测试之间可能需要重置它来保证隔离性。源码注释"Reset a specific singleton — primarily for testing"与其 API 文档说明一致。
仓库中的真实应用:HttpSessionManager
SingletonSupport 在仓库中最直接的落地场景是 HTTP 会话管理器 HttpSessionManager.java。
该类是一个 final 类,私有构造函数防止外部直接 new,对外仅暴露静态工厂:
public final class HttpSessionManager extends BaseRefResourceMgr<HttpSession> {
private final SessionConfig defaultConfig = new SessionConfig();
private HttpSessionManager() {
}
public static HttpSessionManager getInstance() {
return SingletonSupport.getInstance(HttpSessionManager.class, HttpSessionManager::new);
}
// ...
}
这里体现了 SingletonSupport 的典型调用范式:
- 方法引用
HttpSessionManager::new作为Supplier传入,实例由工厂惰性创建(lazy initialization),只有在第一次真正调用getInstance()时才构建对象; - 由于
INSTANCES按类缓存,进程内HttpSessionManager全局唯一,所有调用方共享同一套连接池与会话管理状态; - 该管理器内部依赖
ConnectorPoolManager.getInstance().getConnectorPool(type, poolConfig)来获取连接池,最终复用java.net.http.HttpClient,因此单例化可以避免重复创建底层的HttpClient及其连接资源。
测试对单例唯一性的验证
在 ClientsTest.java 中,单例语义被直接断言:
assertThat(Clients.getHttpSessionManager()).isSameAs(HttpSessionManager.getInstance());
isSameAs 断言两者是同一个对象引用(而非 equals 相等),这恰好验证了 SingletonSupport 的核心承诺:无论从哪个入口获取,HttpSessionManager 在进程内只有一个实例。测试套件(如 HttpClientTest、HttpClientPythonParityTest 等)也反复通过 HttpSessionManager.getInstance() 获取同一实例进行资源统计与关闭操作,进一步印证了共享语义。
与同包 Singleton 工具类的差异
com.openjiuwen.core.common.utils 包中还存在另一个工具类 Singleton.java,两者容易混淆,需要区分:
| 对比维度 | SingletonSupport |
Singleton |
|---|---|---|
| 类型 | 泛型抽象基类(abstract class SingletonSupport<T>),供服务类继承 |
工具类(final class Singleton),提供静态方法 |
| 实例缓存 | ConcurrentHashMap<Class<?>, Object> |
LinkedHashMap<Class<?>, Object> + 独立锁对象 |
| 并发策略 | 双重检查锁(无锁快速路径 + 类锁临界区) | 每次访问都 synchronized(粗粒度全量加锁) |
| 额外 API | 仅 getInstance / reset |
getInstance / getExistingInstance / hasInstance / clearAll |
| 典型使用者 | HttpSessionManager(见 clients 包) |
ConnectorPoolManager、clients/http 下的 HttpSessionManager 变体等 |
从源码结构看,Singleton 更贴近 Python 端 openjiuwen/core/common/utils/singleton.py 的原样镜像(含 hasInstance、clearAll 等更丰富的能力),而 SingletonSupport 是为 Java 服务类型量身定做的、聚焦"创建 + 重置"两个核心动作的轻量基类。两者并存说明框架在单例基础设施上有两种粒度:需要查询/清空能力时用 Singleton,需要继承式服务单例时用 SingletonSupport。
如何在自有服务类中使用 SingletonSupport
继承并暴露静态工厂
参照 HttpSessionManager 与源码 Javadoc 中的示例,一个完整的自定义单例服务如下:
public class MyService extends SingletonSupport<MyService> {
private MyService() {
// 私有构造:防止外部直接实例化
}
public static MyService getInstance() {
return SingletonSupport.getInstance(MyService.class, MyService::new);
}
}
要点:
- 继承时泛型实参填自身
MyService,使getInstance()返回类型安全; - 构造函数设为
private,配合静态工厂形成受控的创建入口; Supplier使用构造方法引用MyService::new,实现惰性初始化——首次调用getInstance()才真正创建实例;- 若实例初始化成本较高或依赖外部配置,也可在
factory中传入带参数的 Lambda(如() -> new MyService(config)),此时clazz仍传MyService.class。
测试中重置单例
由于单例跨测试用例共享状态,测试之间可能互相污染。此时使用:
SingletonSupport.reset(MyService.class);
reset 会移除该类的缓存实例,下一次 getInstance() 将重新走创建流程,得到全新的对象。这正是文档中"主要用于测试"的实践含义——它让开发者在不重启 JVM 的前提下刷新单例状态,保证测试隔离性。
设计要点总结
- 单例粒度:
INSTANCES以Class<?>为键,每个具体类只保存一个对象,因此单例粒度是"每个具体类一个实例"。不同类(哪怕共享同一父类)拥有各自独立的实例,互不干扰。 - 线程安全:
ConcurrentHashMap提供无锁读路径,synchronized临界区内的二次检查杜绝重复创建,构成标准的双重检查锁(Double-Checked Locking)模式。 - 惰性创建:实例仅在首次
getInstance()时通过Supplier创建,避免类加载阶段即完成初始化的开销。 - 可测试性:
reset(Class<?>)提供了显式的生命周期重置入口,弥补了传统单例难以在测试间隔离的短板。 - 框架价值:在 Agent 框架中,LLM 客户端、连接池、会话管理器等资源密集组件通过单例共享,能显著降低资源重复创建成本,同时保证全局配置的一致性。
小结
SingletonSupport 虽是一个仅含两个静态方法的轻量基类,却承担了框架内服务组件生命周期管理的核心职责:以双重检查锁保证并发安全,以"按具体类缓存"保证实例唯一,以 reset 支持测试隔离。理解它的实现与用法,你既能看懂 HttpSessionManager 等既有组件的单例语义,也能在自己的 Agent 服务类中复用同一套经过验证的基础设施。