本文目录索引:
1、概要 2、 构造方法和初始化 3、 get 操作 4、put操作 5、rehash操作 6、remove操作 7、 ConcurrentHashMap 的弱一致性 8、 size 、 containsValue1、概要
ConcurrentHashMap 是由 Segment 数组结构和 HashEntry 数组结构组成。
Segment 是一种可重入锁(ReentrantLock),在 ConcurrentHashMap 里扮演锁的
角色;HashEntry 则用于存储键值对数据。一个 ConcurrentHashMap 里包含一个
Segment 数组。 Segment 的结构和 HashMap 类似,是一种数组和链表结构。一个 Segment 里包含一个 HashEntry 数组,每个 HashEntry 是一个链表结构的元素, 每个 Segment 守护着一个 HashEntry 数组里的元素,当对 HashEntry 数组的数据 进行修改时,必须首先获得与它对应的 Segment 锁。
2、构造方法和初始化
ConcurrentHashMap 初始化方法是通过 initialCapacity、loadFactor 和
concurrencyLevel( 参数 concurrencyLevel 是用户估计的并发级别,就是说你觉得最 多有多少线程共同修改这个 map ,根据这个来确定 Segment 数组的大小 concurrencyLevel 默认是 DEFAULT_CONCURRENCY_LEVEL = 16;) 等几个参数来初始 化 segment 数组、段偏移量 segmentShift 、段掩码 segmentMask 和每个 segment 里的 HashEntry 数组来实现的。 并发级别可以理解为程序运行时能够同时更新 ConccurentHashMap 且不产 生锁竞争的最大线程数,实际上就是 ConcurrentHashMap 中的分段锁个数,即 Segment[] 的数组长度。 ConcurrentHashMap 默认的并发度为 16 ,但用户也可以 在构造函数中设置并发度。当用户设置并发度时, ConcurrentHashMap 会使用大 于等于该值的最小 2 幂指数作为实际并发度(假如用户设置并发度为 17 ,实际 并发度则为 32 )。 如果并发度设置的过小,会带来严重的锁竞争问题;如果并发度设置的过大, 原本位于同一个 Segment 内的访问会扩散到不同的 Segment 中, CPU cache 命中 率会下降,从而引起程序性能下降。(文档的说法是根据你并发的线程数量决定, 太多会导性能降低) segments 数组的长度 ssize 是通过 concurrencyLevel 计算得出的。为了能通 过按位与的散列算法来定位 segments 数组的索引,必须保证 segments 数组的长 度是 2 的 N 次方( power-of-two size ),所以必须计算出一个大于或等于 concurrencyLevel 的最小的 2 的 N 次方值来作为 segments 数组的长度。假如 concurrencyLevel 等于 14 、 15 或 16 , ssize 都会等于 16 ,即容器里锁的个数也是 16 。 初始化 segmentShift 和 segmentMask 这两个全局变量需要在定位 segment 时的散列算法里使用, sshift 等于 ssize 从 1 向左移位的次数,在默认情况下 concurrencyLevel 等于 16 , 1 需要向左移位 移动 4 次,所以 sshift 等于 4 。 segmentShift 用于定位参与散列运算的位数, segmentShift 等于 32 减 sshift ,所以等于 28 ,这里之所以用 32 是因为 ConcurrentHashMap 里的 hash() 方法输出的最大数是 32 位的。 segmentMask 是散 列运算的掩码,等于 ssize 减 1 ,即 15 ,掩码的二进制各个位的值都是 1 。因为 ssize 的最大长度是 65536 ,所以 segmentShift 最大值是 16 , segmentMask 最大值 是 65535 ,对应的二进制是 16 位,每个位都是 1 。
输入参数
initialCapacity
是
ConcurrentHashMap
的初始化容量,
loadfactor
是
每个
segment
的负载因子,在构造方法里需要通过这两个参数来初始化数组中的
每个
segment
。上面代码中的变量
cap
就是
segment
里
HashEntry
数组的长度,
它等于
initialCapacity
除以
ssize
的倍数
c
,如果
c
大于
1
,就会取大于等于
c
的
2
的
N
次方值,所以
segment
里
HashEntry
数组的长度不是
1
,就是
2
的
N
次方。
在默认情况下,
ssize = 16
,
initialCapacity = 16
,
loadFactor = 0.75f
,那么
cap
= 1
,
threshold = (int) cap * loadFactor = 0
。
既然
ConcurrentHashMap
使用分段锁
Segment
来保护不同段的数据,那么在
插入和获取元素的时候,必须先通过散列算法定位到
Segment
。
ConcurrentHashMap
会首先使用
Wang/Jenkins hash
的变种算法对元素的
hashCode
进行一次再散列。
ConcurrentHashMap
完全允许多个读操作并发进行,读操作并不需要加锁。
ConcurrentHashMap
实现技术是保证
HashEntry
几乎是不可变的以及
volatile
关键
字。
3、get 操作

4、put操作

put
方法会通过
tryLock()
方法尝试获得锁,获得了锁,
node
为
null
进入
try
语句块,没有获得锁,调用
scanAndLockForPut
方法自旋等待获得锁。
scanAndLockForPut
方法里在尝试获得锁的过程中会对对应
hashcode
的链表
进行遍历,如果遍历完毕仍然找不到与
key
相同的
HashEntry
节点,则为后续的
put
操作提前创建一个
HashEntry
。当
tryLock
一定次数后仍无法获得锁,则通过
lock
申请锁。

否则新建一个
HashEntry
节点,采用头插法,将它设置为链表的新
head
节
点并将原头节点设为新
head
的下一个节点。新建过程中如果节点总数(含新建
的
HashEntry
)超过
threshold
,则调用
rehash()
方法对
Segment
进行扩容,最后
将新建
HashEntry
写入到数组中。
5、rehash操作
扩容是新创建了数组,然后进行迁移数据,最后再将 newTable 设置给属性 table 。 为了避免让所有的节点都进行复制操作:由于扩容是基于 2 的幂指来操作, 假设扩容前某 HashEntry 对应到 Segment 中数组的 index 为 i ,数组的容量为 capacity ,那么扩容后该 HashEntry 对应到新数组中的 index 只可能为 i 或者 i+capacity ,因此很多 HashEntry 节点在扩容前后 index 可以保持不变。 假设原来 table 长度为 4 ,那么元素在 table 中的分布是这样的
可以看见
hash
值为
34
和
56
的下标保持不变,而
15,23,77
的下标都是在原
来下标的基础上
+4
即可,可以快速定位和减少重排次数。
该方法没有考虑并发,因为执行该方法之前已经获取了锁。





