已经有 HashMap 和 Hashtable 了,ConcurrentHashMap 解决了什么问题?

112 阅读2分钟

这是我参与11月更文挑战的第12天,活动详情查看:2021最后一次更文挑战

HashMap 位于 java.util 包,是 Java 集合框架中最常用的数据结构之一,用来存储键值对,实现 O(1) 复杂度的读写。但是 HashMap 不是一个线程安全的数据结构。同样在 java.util 包下,有一个 HashMap 的线程安全版本,就是 Hashtable。它解决线程安全问题的方式十分粗暴,就是在 HashMap 的基础上,在 put、get、size 等方法上加 synchronized 关键字。这其实会大大降低并发操作的效率,因此,它只适合并不是非常高的并发场景。

对于这个问题,Java 还提供了 ConcurrentHashMap,从它的名字可以看出,它是面向并发编程设计的。它使用了分离锁,也就是将内部进行分段(Segment),里面则是 HashEntry 的数组,和 HashMap 类似,哈希相同的条目也是以链表形式存放。

在 JDK 1.8 之前

当进行 put 操作的时候,会分段加锁 Segment。一个 ConcurrentHashMap 中所有的桶会分成若干个 Segment,数据通过哈希操作得到要加入的 Segment,然后对 Segment 进行加锁,然后再进行哈希计算,得到要加入的桶,将新增数据加入到桶中。

相较于对整个 Map 进行加锁,分段加锁的影响范围会小很多,大大降低了枷锁操作对并发效率造成的影响。

当进行 size 操作的时候,ConcurrentHashMap 会进行两次分段计算,如果两次的结果相同,则返回,否则对所有 Segment 枷锁进行重新计算。

JDK 1.8 及之后

在逻辑上不再有 Segment 的概念,仅为了序列化的兼容性做了保留,它的结构更接近 HashMap。

当进行 put 操作时,会首先判断容器是否已经初始化,只有当第一次放入数据的时候,才会初始化容器,有效地避免了不必要的开销。如果判断不为空,则利用 CAS 设置新节点。如果 put 数据时,对应的桶不为空,则会使用 synchronized 对桶进行加锁,遍历桶中的链表或者红黑树,在桶中替换或者新增数据。

相较于之前的分段加锁,它的锁粒度更细,这是新旧版本 ConcurrentHashMap 最大的特征差别。