Java多线程:(四)详解ConcurrentHashMap构成
一、JDK 1.7
1.1 加锁机制
ConcurrentHashMap 在 JDK 1.7 中,提供了一种粒度更细的加锁机制,这种机制叫分段锁「Lock Striping」。整个哈希表被分为多个段,每个段都独立锁定。读取操作不需要锁,写入操作仅锁定相关的段。这减小了锁冲突的几率,从而提高了并发性能。
这种机制的优点:在并发环境下将实现更高的吞吐量,而在单线程环境下只损失非常小的性能。
可以这样理解分段锁,就是将数据分段,对每一段数据分配一把锁。当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问。
有些方法需要跨段,比如 size()
、isEmpty()
、containsValue()
,它们可能需要锁定整个表而不仅仅是某个段,这需要按顺序锁定所有段,操作完后,再按顺序释放所有段的锁。如下图:
1.2 jdk1.7之前的结构
ConcurrentHashMap 是由 Segment 数组结构和 HashEntry 数组+链表构成的。Segment 是一种可重入的锁 ReentrantLock
,HashEntry 则用于存储键值对数据。
一个 ConcurrentHashMap 里包含一个 Segment 数组,数组中都是segment对象,Segment 的结构和 HashMap 类似,是一种数组和链表结构,一个 Segment 里包含一个 HashEntry 数组,每个 HashEntry元素 是一个链表结构的元素,每个 Segment 守护着一个 HashEntry 数组里的元素,当对 HashEntry 数组的数据进行修改时,必须首先获得它对应的 Segment 锁(可重入锁)。
ConcurrentHashMap
使用链表来存储这些具有相同索引值的键值对。具体来说,每个数组位置(在JDK 1.7中实际上是Segment
内部的数组,而在JDK 1.8及以后是Node
数组)存储的是链表的头节点。如果发生哈希冲突,新的键值对会被添加到这个链表的末尾。
接下来看看Segment的结构组成:
单一的 Segment 结构如下:
像这样的 Segment 对象,在 ConcurrentHashMap 集合中有多少个呢?有 2 的 N 次方个,共同保存在一个名为 segments 的数组当中。 因此整个 ConcurrentHashMap 的结构如下:
可以说,ConcurrentHashMap 是一个二级哈希表。在一个总的哈希表下面,有若干个子哈希表。
Case1:不同 Segment 的并发写入(可以并发执行)
Case2:同一 Segment 的一写一读(可以并发执行)
Case3:同一 Segment 的并发写入
二、JDK1.8往后
而在 JDK 1.8 中,ConcurrentHashMap 主要做了两个优化:
- 同
HashMap
一样,链表也会在长度达到 8 的时候转化为红黑树,这样可以提升大量冲突时候的查询效率; - 以某个位置的头结点(链表的头结点或红黑树的 root 结点)为锁,**配合自旋+CAS(乐观锁)**避免不必要的锁开销,进一步提升并发性能。
相比 JDK1.7 中的 ConcurrentHashMap,JDK1.8 中的 ConcurrentHashMap 取消了 Segment 分段锁,采用 CAS + synchronized 来保证并发安全性,整个容器只分为一个 Segment,即 table 数组。
JDK1.8 中的 ConcurrentHashMap 对节点 Node 类中的共享变量,和 JDK1.7 一样,使用 volatile 关键字,保证多线程操作时,变量的可见性。
HashMap与ConcurrentHashMap工作原理、区别和总结_hashmap和concurrentmap实现原理区别-CSDN博客