如何为Cleaner托管的资源编写线程安全的所有权释放方法
Kotlin 基于 Cleaner 管理原生资源的 release 方法实现问题
现有封装代码
private val CLEANER = java.lang.ref.Cleaner.create() internal typealias Ptr = Long private external fun acquireResource(): Ptr private external fun disposeResource(nativePtr: Ptr) /* ... */ class Resource private constructor (private val nativePtr: Ptr): Closeable, AutoCloseable { companion object { fun create(): Resource { return Resource(acquireResource()) } } private val cleanable = CLEANER.register(this, object: Runnable { private val nativePtr = this@Resource.nativePtr override fun run() { disposeResource(nativePtr) } }) override fun close() { cleanable.clean() } /* ... */ }
需求与疑问
需要新增 internal fun release(): Ptr 方法,实现以下能力:
- 调用方法后,当前
Resource对象放弃对持有的nativePtr的所有权,直接返回该指针给调用方 - 后续该
Resource对象被垃圾回收时,不能执行nativePtr的释放逻辑 - 设计前提是调用过
release的Resource对象不会再被使用,目标是把拿到的Ptr交给后续负责管理该指针生命周期的 API - 要求实现为原子、线程安全,需确认该需求是否与
Cleaner机制兼容 - 本质是要实现等价于 JavaScript 中
FinalizationRegistry.prototype.unregister的解绑能力
实现方案
完全不需要依赖 Cleaner 提供类似 JS 的 unregister 接口,通过原子状态标记配合现有 Cleaner 即可实现需求,且天然线程安全、无竞态问题。
核心修正点
原代码存在两个关键问题:
Runnable回调中额外拷贝了nativePtr的值,导致后续Resource内部的指针状态变更无法同步到回调逻辑,会直接破坏 release 后的所有权语义- 没有对指针的所有权转移做原子状态控制,多线程下会出现重复释放、所有权混乱的问题
具体实现
- 将原
nativePtr字段替换为AtomicLong类型,约定值为 0 时代表“已转移所有权,无需释放”,所有涉及所有权变更的操作都通过getAndSet(0)原子完成,保证全局只有一个路径能拿到非 0 的指针值 - 重写 Cleaner 注册的回调逻辑,不再额外持有指针副本,回调触发时同样走原子状态判断,仅当拿到非 0 旧值时才执行释放逻辑
- 重写
close方法,新增release方法,两个方法都复用同一套原子状态判断逻辑
最终修正后的代码(需要导入 java.util.concurrent.atomic.AtomicLong):
private val CLEANER = java.lang.ref.Cleaner.create() internal typealias Ptr = Long private external fun acquireResource(): Ptr private external fun disposeResource(nativePtr: Ptr) class Resource private constructor(ptr: Ptr): Closeable, AutoCloseable { // 用AtomicLong管理所有权,0代表无所有权 private val nativePtr = AtomicLong(ptr) companion object { fun create(): Resource { return Resource(acquireResource()) } } // 注册Cleaner回调,不再额外持有指针副本 private val cleanable = CLEANER.register(this, Runnable { val oldPtr = nativePtr.getAndSet(0L) if (oldPtr != 0L) { disposeResource(oldPtr) } }) override fun close() { val oldPtr = nativePtr.getAndSet(0L) if (oldPtr != 0L) { disposeResource(oldPtr) } // 主动触发clean,移除注册跟踪,减少Cleaner内部开销 cleanable.clean() } internal fun release(): Ptr { val oldPtr = nativePtr.getAndSet(0L) // 指针为0说明已经被close或之前的release处理过,直接抛错 check(oldPtr != 0L) { "Resource already released or closed" } // 主动移除注册,无需再被Cleaner跟踪 cleanable.clean() return oldPtr } }
方案合理性说明
- 原子线程安全:所有涉及所有权转移的操作都是
AtomicLong.getAndSet,这是CPU级别的原子CAS操作,多线程同时调用close、release或GC触发Cleaner回调时,只会有一个执行路径拿到非0的旧指针值,其余路径拿到0值直接跳过,不存在双重释放、所有权抢夺的竞态 - 与Cleaner完全兼容:本质是通过状态标记让Cleaner触发的回调自动变为空操作,等价于实现了解绑效果——就算后续Resource被GC触发回调,回调也会因为检测到指针值为0而不执行释放逻辑
- 兜底防泄漏:如果用户既不主动close也不release,等Resource被GC时,Cleaner会自动触发回调释放指针,不会出现原生资源泄漏
边界竞态覆盖
所有极端场景都被原子操作兜底:
- 若Cleaner回调先拿到非0指针完成释放,后续调用
release时会检测到指针值为0,直接抛出状态错误,符合预期 - 若
release先拿到非0指针完成所有权转移,后续Cleaner回调触发时会检测到指针值为0,不执行任何操作,保证转移出去的指针不会被意外释放
内容的提问来源于stack exchange,提问作者dumbass
相关产品推荐
相关产品推荐

