You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何修复Kotlin‘获取或创建’缓存中的竞态条件?

如何修复Kotlin‘获取或创建’缓存中的竞态条件?

哥们,我之前也踩过这个「检查-创建-缓存」的竞态条件坑!你的代码问题出在:多个线程可以同时通过缓存检查的步骤,然后都跑去执行昂贵的createResource,最后不仅浪费资源,还可能导致缓存里的实例和线程返回的实例不一致。下面我给你唠唠Kotlin里常用的几种解决办法,还有各自的优缺点,你可以根据自己的场景选:

首先先明确问题根源:你的getResource里,缓存检查和更新不是原子操作——当线程A还在执行createResource的时候,线程B可能已经通过了缓存检查,也开始创建资源,最终导致同一个id的资源被创建多次。


1. 最省心的基础方案:用synchronized同步块

最简单粗暴的方式,给整个缓存检查+创建的逻辑加锁,保证同一时间只有一个线程能执行这段代码。

class ResourceProvider {
    private var cachedResource: CachedResource? = null
    
    private fun createResource(id: String): CachedResource {
        println("Creating new resource for id: $id...")
        Thread.sleep(1000) // 模拟耗时操作
        return CachedResource(id)
    }

    fun getResource(id: String): CachedResource {
        synchronized(this) { // 加锁,同一时间仅一个线程能进入
            val localCache = cachedResource
            if (localCache != null && localCache.id == id) {
                return localCache
            }
            val newResource = createResource(id)
            cachedResource = newResource
            return newResource
        }
    }
}

优点:实现简单,逻辑清晰,完全杜绝竞态条件,新手也能快速理解。
缺点:每次调用getResource都要抢锁,哪怕缓存已经存在(命中),高并发场景下会因为锁竞争导致线程阻塞,拖慢性能。


2. 性能优化方案:双重检查锁定(Double-Checked Locking)

为了解决同步块的性能问题,我们可以做「两次检查」:先无锁快速检查缓存,只有当缓存无效时才加锁,加锁后再检查一次(防止加锁期间其他线程已经创建了资源)。

注意:这里必须给cachedResource加@Volatile注解,不然多线程环境下可能出现指令重排序,导致线程拿到半初始化的CachedResource实例。

class ResourceProvider {
    @Volatile // 必须加,保证内存可见性+禁止指令重排序
    private var cachedResource: CachedResource? = null
    
    private fun createResource(id: String): CachedResource {
        println("Creating new resource for id: $id...")
        Thread.sleep(1000)
        return CachedResource(id)
    }

    fun getResource(id: String): CachedResource {
        var localCache = cachedResource
        // 第一次无锁检查,缓存命中直接返回
        if (localCache != null && localCache.id == id) {
            return localCache
        }
        // 缓存无效,加锁
        synchronized(this) {
            // 第二次检查,防止加锁期间其他线程已经创建了资源
            localCache = cachedResource
            if (localCache != null && localCache.id == id) {
                return localCache
            }
            val newResource = createResource(id)
            cachedResource = newResource
            return newResource
        }
    }
}

优点:缓存命中时不需要抢锁,性能比全同步块好很多,只有缓存未命中时才会有锁竞争。
缺点:代码复杂度略高,需要注意@Volatile的使用;而且这个方案只适合缓存单一实例,如果需要缓存多个不同id的资源,就不适用了。


3. 灵活扩展方案:ConcurrentHashMap+lazy委托

如果你的需求是缓存多个不同id的资源(而不是单一实例),可以用ConcurrentHashMap存储每个id对应的lazy实例。Kotlin的lazy默认是线程安全的,能保证每个id对应的资源只会被创建一次。

class ResourceProvider {
    // 用ConcurrentHashMap存储每个id对应的懒加载资源
    private val resourceCache = ConcurrentHashMap<String, Lazy<CachedResource>>()
    
    private fun createResource(id: String): CachedResource {
        println("Creating new resource for id: $id...")
        Thread.sleep(1000)
        return CachedResource(id)
    }

    fun getResource(id: String): CachedResource {
        // 原子性获取或创建对应id的lazy实例
        val lazyResource = resourceCache.computeIfAbsent(id) {
            lazy { createResource(it) }
        }
        // 调用value时才会触发createResource(仅第一次调用)
        return lazyResource.value
    }
}

优点:

  • 天然支持多id缓存,扩展性极强;
  • 代码简洁,不用手动处理锁,利用Kotlin的委托特性和ConcurrentHashMap的原子操作实现线程安全;
  • lazy的线程安全模式可配置,默认的同步模式完全满足需求。

缺点:

  • 缓存的资源会一直存在(除非手动清理resourceCache),如果id数量很多,可能会占用较多内存;
  • 第一次获取新id资源时,会有一点点computeIfAbsent和lazy初始化的额外开销,不过大多数场景下可以忽略。

4. 不推荐的方案:用AtomicReference的CAS操作

如果想完全避免显式锁,可以用AtomicReference的compareAndSet方法原子更新缓存,但这个方案有个致命问题:无法避免createResource被多次调用(只是保证缓存最终是正确的),会浪费资源,所以一般不推荐。

import java.util.concurrent.atomic.AtomicReference

class ResourceProvider {
    private val cachedResource = AtomicReference<CachedResource?>()
    
    private fun createResource(id: String): CachedResource {
        println("Creating new resource for id: $id...")
        Thread.sleep(1000)
        return CachedResource(id)
    }

    fun getResource(id: String): CachedResource {
        var current = cachedResource.get()
        if (current != null && current.id == id) {
            return current
        }
        val newResource = createResource(id)
        // 尝试原子更新缓存,如果当前缓存还是原来的状态则更新成功
        while (!cachedResource.compareAndSet(current, newResource)) {
            current = cachedResource.get()
            if (current != null && current.id == id) {
                return current
            }
        }
        return newResource
    }
}

优点:无显式锁,低竞争场景下性能尚可;
缺点:代码逻辑复杂难理解,且多个线程同时调用时,createResource仍会被多次执行,浪费资源。


总结选择建议

  • 只缓存单一实例、并发量不极高:选双重检查锁定,平衡性能和代码复杂度;
  • 需要缓存多个id的资源:选ConcurrentHashMap + lazy,代码简洁扩展性好;
  • 追求最简单实现、不介意性能开销:选synchronized同步块;
  • 尽量避开AtomicReference的CAS方案,因为它无法避免资源的重复创建。

内容来源于stack exchange

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.04.07 11:13:05