ConcurrentHashMap 复合操作下丢失原子性
ConcurrentHashMap的线程安全性
ConcurrentHashMap 的线程安全性主要体现在以下几个方面:
-
分段锁:在Java 7及之前的版本中, ConcurrentHashMap 通过分段锁(segment locking)技术来提高并发访问的效率。它将数据分成一段一段的存储,每一段数据拥有自己的锁。当一个线程访问一段数据时,只需获取那一段的锁,这样多个线程可以同时访问不同段的数据,从而提高并发性能。
-
CAS操作:从Java 8开始, ConcurrentHashMap 的实现改进为使用CAS(Compare-And-Swap)操作和synchronized关键字来管理内部结构的并发修改,摒弃了原有的分段锁。这种方法在不牺牲太多性能的同时,简化了代码和内部结构。
原子性的局限
虽然 ConcurrentHashMap 提供了操作的线程安全性,但它并不保证复合操作的原子性。例如,以下操作不是原子的:
map.put(key, map.get(key) + 1);
在这个操作中, map.get(key) 和 map.put(key, value) 分别是线程安全的,但是从 get 取值到 put 存回的过程中间,其他线程可能已经修改了这个key对应的值。这就是所谓的“检查后执行”操作,它需要外部同步来保证整个过程的原子性。
解决方案
为了保证复合操作的原子性,可以使用如下方法:
- 使用 ConcurrentHashMap 的 compute 方法:
ConcurrentHashMap 提供了一些原子性保证的复合操作,如 compute 、 merge 等。这些方法可以用来原子地修改映射中的值。
concurrentMap
.compute(key, (k, v) ->
(v == null) ? 1 : v + 1);
这个例子中, compute 方法会原子地更新键对应的值,这是通过内部锁定或其他形式的并发控制来实现的。
总结来说, ConcurrentHashMap 确实是线程安全的,但线程安全不等同于原子性。对于复合操作,需要使用适当的方法来确保操作的原子性。