openJiuwen agent-core-java 中的 SingletonSupport:线程安全单例创建与重置的通用基础设施

原创2026-10-12 18:12:011,679 阅读
文章标签:人工智能大模型AI AgentAgent 框架工具调用流程编排RAGMCP Clients

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;
}

其执行流程分为三个阶段:

  1. 第一次检查(无锁):先通过 INSTANCES.get(clazz) 快速读取。绝大多数并发场景下实例已经存在,直接命中并返回,无需进入同步块,这也是 ConcurrentHashMap 在高并发读多写少场景下的核心价值。
  2. 加锁后的第二次检查:仅在缓存未命中时才进入 synchronized (SingletonSupport.class) 临界区,并在临界区内再次读取缓存。这一步防止两个线程同时通过第一次检查后重复创建实例——即使两个线程都发现 instance == null,也只有一个线程能持有锁完成创建,另一个线程会在进入临界区后看到已被放入的实例。
  3. 创建与缓存:由调用方传入的 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 服务类中复用同一套经过验证的基础设施。

登录后查看全文
agent-core-java