ConcurrentHashMap.computeIfAbsent内赋值是否线程安全?附缓存实现分析
这是个很棒的问题!咱们先把ConcurrentHashMap.computeIfAbsent的线程安全特性说清楚,再拆解你给出的HandleCache实现。
ConcurrentHashMap.computeIfAbsent的线程安全性
先给核心结论:ConcurrentHashMap的computeIfAbsent方法对同一个key的lambda执行是原子性的。也就是说,当多个线程同时针对同一个key调用这个方法时,只有一个线程会执行传入的lambda初始化逻辑,其他线程会阻塞等待该逻辑执行完成,然后直接复用返回的结果。这是ConcurrentHashMap为并发场景专门设计的特性,从根源上避免了同一个key被重复初始化的线程安全问题。
HandleCache实现的线程安全分析
咱们逐部分看这个缓存实现的线程安全情况:
- 计数器的线程安全:这里用
AtomicInteger的getAndIncrement()来生成handle,这个方法是原子操作,每次调用都会返回唯一的递增整数,绝对不会出现两个线程拿到相同i值的情况。这就保证了每个新的Data实例都会被分配一个独一无二的数组索引。 - 数组赋值的安全性:因为每个线程拿到的
i都是唯一的,所以不同线程写入的是数组的不同索引位置。而Java中引用类型的数组元素赋值是原子操作(JMM保证引用类型的写操作不会出现“写一半”的情况),所以array[i] = k这一步不会有数据竞争导致的不一致问题。 - computeIfAbsent的原子性兜底:对于同一个
Data实例作为key,只会有一个线程进入lambda逻辑执行,这就彻底避免了同一个Data被多次添加到缓存的情况——不会出现同一个data对应多个handle的矛盾,保证了缓存的一致性。
值得注意的潜在问题
虽然这个实现的线程安全性是可靠的,但有两个点需要留意:
- Data类的equals/hashCode实现:ConcurrentHashMap完全依赖key的
equals()和hashCode()方法来判断key是否存在。如果Data类没有正确实现这两个方法,可能会导致逻辑上相同的Data被当成不同的key,从而重复分配handle,这是正确性问题,而非线程安全问题。 - 数组容量耗尽的限制:当
counter递增到超过数组长度时,会抛出IllegalStateException,此时缓存无法再接受新的Data实例。如果你的业务场景需要缓存更多数据,可能需要考虑数组扩容或者引入缓存淘汰策略。
内容的提问来源于stack exchange,提问作者Jörn Horstmann
相关产品推荐
相关产品推荐

