You need to enable JavaScript to run this app. 如何为Cleaner托管的资源编写线程安全的所有权释放方法
优惠活动
大模型
产品
解决方案
定价
更多

如何为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 即可实现需求,且天然线程安全、无竞态问题。

核心修正点

原代码存在两个关键问题:

  1. Runnable 回调中额外拷贝了 nativePtr 的值,导致后续 Resource 内部的指针状态变更无法同步到回调逻辑,会直接破坏 release 后的所有权语义
  2. 没有对指针的所有权转移做原子状态控制,多线程下会出现重复释放、所有权混乱的问题

具体实现

  1. 将原 nativePtr 字段替换为 AtomicLong 类型,约定值为 0 时代表“已转移所有权,无需释放”,所有涉及所有权变更的操作都通过 getAndSet(0) 原子完成,保证全局只有一个路径能拿到非 0 的指针值
  2. 重写 Cleaner 注册的回调逻辑,不再额外持有指针副本,回调触发时同样走原子状态判断,仅当拿到非 0 旧值时才执行释放逻辑
  3. 重写 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会自动触发回调释放指针,不会出现原生资源泄漏

边界竞态覆盖

所有极端场景都被原子操作兜底:

  1. 若Cleaner回调先拿到非0指针完成释放,后续调用 release 时会检测到指针值为0,直接抛出状态错误,符合预期
  2. 若 release 先拿到非0指针完成所有权转移,后续Cleaner回调触发时会检测到指针值为0,不执行任何操作,保证转移出去的指针不会被意外释放

内容的提问来源于stack exchange,提问作者dumbass

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.11.04 20:54:06