Skip to content

对象池

ObjectPool<T> 复用高频创建/销毁的短生命周期对象,减少 GC 压力,适用于 StringBuilder、缓冲区、临时容器等场景。

适用场景

  • Chat 应用里每次请求都要新建 StringBuilder 拼装提示词,用后即弃。
  • 高频创建和销毁同一类型的临时对象(缓冲区、序列化中间容器、请求上下文包装器)。
  • 对象创建成本不算极高,但创建频率极高,导致 GC 触发过于频繁。
  • 对象的状态可以在归还前重置到干净状态,下次取出即可直接使用。

如果你的场景是"创建一次、长期持有",对象池不是正确答案——这种情况应该直接用依赖注入管理生命周期。

相关依赖

包名作用什么时候需要
soulsoft_extensions_objectpool提供 ObjectPool<T>ObjectPoolFactoryDefaultObjectPoolProviderIPooledObjectPolicy<T> 等核心类型始终需要

快速开始

先引入 soulsoft_extensions_objectpool,按"创建池 → 借出 → 使用 → 归还"这条最小路径走一遍。

cangjie
import soulsoft_extensions_objectpool.*

class ChatPromptBuilder {
    public var segments: Int64 = 0

    public func append(text: String): Unit {
        segments += 1
    }
}

// 一行创建对象池,工厂函数定义"怎么创建新对象"
let pool = ObjectPoolFactory.create { => ChatPromptBuilder() }

// 借出 → 使用 → 归还
let builder = pool.get()
builder.append("system: you are a helpful assistant")
builder.append("user: hello")
// 使用完毕,归还前重置状态
builder.segments = 0
pool.put(builder)

ObjectPoolFactory.create() 是最简入口:传入一个工厂 lambda,返回一个可用的 ObjectPool<T>

角色职责常用入口
ObjectPoolFactory静态工厂,一行创建对象池create { => T() }
ObjectPool<T>对象池抽象,提供 get() / put()pool.get() / pool.put(obj)
DefaultObjectPoolProvider可配置最大保留数的创建器provider.maximumRetained = 32

核心概念

soulsoft_extensions_objectpool 提供两层能力:

  • ObjectPool<T>:统一 get() / put() 两个操作,封装对象的借出和归还。
  • IPooledObjectPolicy<T>:策略接口,控制对象的创建方式和归还条件。

这层抽象的价值在于把"怎么创建""能不能复用""归还时做什么"这三件事收敛到策略里,调用方只需要 get()put()

业务代码 ──get / put──▶ ObjectPool<T> ──create / release──▶ IPooledObjectPolicy<T>
                              │                                    │
                              │                                    ├──▶ DefaultPooledObjectPolicy(默认,识别 IResettable)
                              │                                    │
                              │                                    └──▶ 自定义策略

                              └──▶ DefaultObjectPool<T>(默认实现,快速槽位 + 并发队列)

这张图只表达一条主线:业务代码依赖 ObjectPool<T>get / put,策略决定对象如何创建和是否保留。默认实现基于工厂函数 + IResettable 自动识别,底层用快速槽位 CAS + 并发队列保证线程安全。

信息

soulsoft_extensions_objectpool 是零外部依赖的独立包,不依赖 DI 容器。你可以直接 new 出来用,也可以手动注册进容器。

借出与归还

对象池的核心语义只有两条:

  • get():优先从池中取出已有对象,池空时才创建新对象。
  • put(obj):将对象归还给池,由策略决定保留还是丢弃。

归还后的对象不应再被调用方使用——它的生命周期已经交还给池。

策略决定复用

IPooledObjectPolicy<T> 定义了两个方法:

方法作用调用时机
create()创建新对象实例池中无可用对象时被 get() 调用
release(obj)重置对象并决定是否保留put() 归还时调用,返回 true 保留、false 丢弃

策略把"能不能复用"的判断逻辑从业务代码里抽出来,让调用方不需要关心对象归还后的具体处理。

IResettable:让对象自己知道怎么重置

如果对象实现了 IResettableDefaultPooledObjectPolicy<T> 会在归还时自动调用 tryReset()

cangjie
import soulsoft_extensions_objectpool.*

class ChatTransportOptions <: IResettable {
    public var endpoint: String = ""
    public var timeout: Int64 = 0

    public func tryReset(): Bool {
        endpoint = ""
        timeout = 0
        return true // 返回 true 表示重置成功,可以回池
    }
}

这样设计的原因是:重置逻辑和对象本身绑定,策略不需要知道每种类型的具体重置方式。tryReset() 返回 false 时,该对象会被丢弃而不是放回池中。

最大保留数

DefaultObjectPoolProvider.maximumRetained 控制池中最多保留多少个空闲对象。默认值是 getProcessorCount() * 2,即 CPU 核心数的两倍。

当归还的对象超过这个上限时,多余的对象会被直接丢弃,不会无限堆积。

可释放对象的自动清理

如果池化对象实现了 Resource 接口,DefaultObjectPoolProvider 会自动创建 DisposableObjectPool<T>

  • 池关闭时,所有保留对象会被依次 close()
  • 归还被拒绝的对象也会被 close(),防止资源泄漏。

这意味着数据库连接、文件句柄等实现了 Resource 的对象,在池化时可以安全释放。

常见用法

直接用工厂函数创建

这是最简路径,适合大多数场景。先看 ObjectPoolFactory.create() 一行完成创建。

cangjie
import soulsoft_extensions_objectpool.*

class ChatSession {
    public var messageCount: Int64 = 0
}

let pool = ObjectPoolFactory.create { => ChatSession() }

let session = pool.get()
session.messageCount = 3
session.messageCount = 0 // 归还前手动重置
pool.put(session)

这段代码只做了三件事:创建池、借出对象、归还对象。工厂函数 { => ChatSession() } 只在池空时被调用。

实现 IResettable 自动重置

如果不想在每次 put() 前手动重置,让对象实现 IResettable

cangjie
import soulsoft_extensions_objectpool.*

class ChatPromptBuilder <: IResettable {
    public var length: Int64 = 0
    public var content: String = ""

    public func tryReset(): Bool {
        length = 0
        content = ""
        return true
    }
}

let pool = ObjectPoolFactory.create { => ChatPromptBuilder() }

let builder = pool.get()
builder.content = "system prompt"
builder.length = builder.content.runes().count()
pool.put(builder) // tryReset() 自动被调用,无需手动重置

关键区别:put() 时策略自动调用 tryReset(),调用方不需要在归还前手动清空字段。tryReset() 返回 true 表示对象已重置干净,可以回池;返回 false 表示该对象不应复用。

自定义策略控制创建和归还

当工厂函数不够用——比如创建时需要初始配置、归还时需要校验对象状态——用自定义策略。

cangjie
import soulsoft_extensions_objectpool.*

class ChatTransportOptions {
    public var endpoint: String = ""
    public var retryCount: Int64 = 0
}

class ChatTransportPolicy <: IPooledObjectPolicy<ChatTransportOptions> {
    public func create(): ChatTransportOptions {
        let opts = ChatTransportOptions()
        opts.endpoint = "https://api.default"
        return opts
    }

    public func release(obj: ChatTransportOptions): Bool {
        // 归还前校验:重试次数过多的对象不再复用
        if (obj.retryCount > 3) {
            return false // 丢弃
        }
        obj.endpoint = ""
        obj.retryCount = 0
        return true // 保留
    }
}

let pool = ObjectPoolFactory.create(ChatTransportPolicy()) { => ChatTransportOptions() }

自定义策略的核心价值在于 release() 可以加归还条件——不是所有对象都值得复用。上面这种写法适合"对象在使用过程中可能进入不可复用状态"的场景。

使用 DefaultObjectPoolProvider 控制最大保留数

需要调整池容量时,用 DefaultObjectPoolProvider 替代 ObjectPoolFactory

cangjie
import soulsoft_extensions_objectpool.*

let provider = DefaultObjectPoolProvider()
provider.maximumRetained = 16 // 覆盖默认值(CPU 核心数 × 2)

class ChatSession {
    public var id: Int64 = 0
}

let pool = provider.create { => ChatSession() }

区别在于:ObjectPoolFactory 是静态工厂,一行搞定;DefaultObjectPoolProvider 可以调整 maximumRetained,适合需要控制内存占用的场景。

复用 StringBuilder

字符串拼接是对象池最典型的应用场景。createStringBuilderPool() 提供了开箱即用的 StringBuilder 池。

cangjie
import soulsoft_extensions_objectpool.*

let provider = DefaultObjectPoolProvider()
let sbPool = provider.createStringBuilderPool(
    initialCapacity: 128,  // 归还时容量回缩到这个值
    maximumRetainedCapacity: 4096 // 容量超过此值的对象不保留
)

let sb = sbPool.get()
sb.append("Chat request from ")
sb.append("user: alice")
let prompt = sb.toString()
sbPool.put(sb)

警告

归还 StringBuilder 时,策略会调用 reset(capacity: initialCapacity)——这会清空内容,同时把容量回缩到初始容量,而不是保留归还前的容量。如果后续拼接长度接近上次,可能会触发一次扩容。

与依赖注入集成

ObjectPool 本身不依赖 DI,但可以手动注册进容器。先看 ChatService 如何通过构造函数接收池,再看注册代码。

cangjie
import soulsoft_extensions_objectpool.*
import soulsoft_extensions_injection.*

class ChatService {
    private let sbPool: ObjectPool<StringBuilder>

    public init(sbPool: ObjectPool<StringBuilder>) {
        this.sbPool = sbPool
    }

    public func buildPrompt(userMessage: String): String {
        let sb = sbPool.get()
        sb.append("system: you are a helpful assistant\n")
        sb.append("user: ${userMessage}")
        let prompt = sb.toString()
        sbPool.put(sb)
        return prompt
    }
}

// 注册阶段:手动创建池并注册为 Singleton
let provider = DefaultObjectPoolProvider()
let sbPool = provider.createStringBuilderPool(
    initialCapacity: 256,
    maximumRetainedCapacity: 4096
)

let services = ServiceCollection()
services.addSingleton(sbPool) // 注册已有实例
services.addTransient<ChatService, ChatService>()

addSingleton(sbPool) 把池注册为单例——整个应用共享同一个池。ChatService 通过构造函数接收 ObjectPool<StringBuilder>,不关心池是怎么创建的。

最佳实践

  • 优先用 ObjectPoolFactory.create { => T() } 起步。只有需要控制池容量或将"能否复用"的判断集中管理时,再引入自定义策略或 DefaultObjectPoolProvider。这样做是因为最简单的路径已经覆盖了 80% 的场景,过早引入策略和 Provider 只会增加不必要的概念负担。
  • 让对象实现 IResettable,把重置逻辑从调用方移到对象内部。目的是让 put() 变成一行,调用方不需要知道"归还前要清哪些字段"。
  • tryReset() 中做完整性校验,不只是在字段上填零。如果对象进入了不可恢复的状态,返回 false 让池丢弃它,这比把脏对象放回池里安全得多。
  • 池化对象不要持有外部资源的引用(文件句柄、网络连接、数据库会话)。如果必须持有,让对象实现 Resource 接口,确保被丢弃时能正确释放。
  • 不要把池注册为 Transient 或 Scoped——池的目的就是跨请求复用对象,短生命周期会让池形同虚设。

推荐组织方式:IResettable 对象 + ObjectPoolFactory.create(),调用方只做 get() → 使用 → put()

cangjie
import soulsoft_extensions_objectpool.*

class ChatPromptSegment <: IResettable {
    public var role: String = ""
    public var content: String = ""

    public func tryReset(): Bool {
        role = ""
        content = ""
        return true
    }
}

let pool = ObjectPoolFactory.create { => ChatPromptSegment() }

let seg = pool.get()
seg.role = "system"
seg.content = "you are a helpful assistant"
// 使用 seg...
pool.put(seg) // tryReset() 自动清空字段

限制与边界

  • 当前包是零依赖的独立包,不提供 services.addObjectPool() 之类的 DI 扩展方法。如需注册进容器,需手动 addSingleton(poolInstance)
  • maximumRetained 默认值是 处理器核心数 × 2,在高并发场景下可能需要调大;但这个值只控制池中保留的空闲对象数上限,不影响并发 get() 时创建新对象。
  • DisposableObjectPool<T> 关闭后,get() 会抛出 IllegalStateExceptionput() 不再保留对象。关闭是幂等的。
  • 池不限制借出对象的总数——如果所有保留对象都被借出且持续不归还,每次 get() 都会创建新对象。对象池解决的是"复用"问题,不是"限流"问题。

警告

对象池不适合管理有外部副作用的对象。如果一个对象的 close() 会断开数据库连接或释放文件锁,务必让对象实现 Resource 并理解 DisposableObjectPool 的关闭语义。

常见问题

get() 每次都返回新对象,池没生效?

最常见的原因是 put() 没有正确调用,或者 release() / tryReset() 返回了 false。检查两个地方:是否每次 get() 后都有对应的 put();策略的 release() 是否在不该丢弃对象时返回了 false

池里应该保留多少对象?

默认值 处理器核心数 × 2 对大多数场景是合理的。如果池的命中率明显偏低(get() 频繁创建新对象),说明保留数不够或归还不及时;先排查归还路径,再调大 maximumRetained

对象池和 DI 的作用域有什么区别?

DI 管理的是"谁持有对象、持有多久"(Singleton / Scoped / Transient),对象池管理的是"对象用完了别扔,下次接着用"。两者解决的问题不同:DI 解决依赖组装,对象池解决内存分配。池本身通常注册为 Singleton,但池里的对象不属于任何 DI 作用域。

StringBuilder 池的容量参数怎么选?

initialCapacity 设为你预期的单次拼接长度,maximumRetainedCapacity 设为你愿意保留的最大容量。超过 maximumRetainedCapacityStringBuilder 在归还时会被丢弃——这是为了防止一个偶然的超长拼接永久占着一大块内存。

相关专题

  • 依赖注入 — 将池实例注册为 Singleton 供业务服务使用
  • 选项模式 — 池的容量等配置可通过选项模式管理
  • 日志记录 — 池的命中率、创建/丢弃统计可通过 ILogger 观测