本文目录索引:
1、概要
2、
构造方法和初始化
3、
get
操作
4、put操作
5、rehash操作
6、remove操作
7、
ConcurrentHashMap
的弱一致性
8、
size
、
containsValue
1、概要
ConcurrentHashMap 是由 Segment 数组结构和 HashEntry 数组结构组成。
Segment 是一种可重入锁(ReentrantLock),在 ConcurrentHashMap 里扮演锁的
角色;HashEntry 则用于存储键值对数据。一个 ConcurrentHashMap 里包含一个
Segment
数组。
Segment
的结构和
HashMap
类似,是一种数组和链表结构。一个
Segment
里包含一个
HashEntry
数组,每个
HashEntry
是一个链表结构的元素,
每个
Segment
守护着一个
HashEntry
数组里的元素,当对
HashEntry
数组的数据
进行修改时,必须首先获得与它对应的
Segment
锁。
![](https://i-blog.csdnimg.cn/blog_migrate/fc4f083ecbbadd2b9f2865e09c49bf52.png)
![](https://i-blog.csdnimg.cn/blog_migrate/9dd8383ab67d3e9464f873492689f836.png)
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
。
![](https://i-blog.csdnimg.cn/blog_migrate/1deed3b5ceebfc9f089410af96f3e038.png)
输入参数
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
关键
字。
![](https://i-blog.csdnimg.cn/blog_migrate/475ed50312c6aa2a3d13152f2b77cd98.png)
3、get 操作
get
操作先经过一次再散列,然后使用这个散列值通过散列运算定位到
Segment(
使用了散列值的高位部分
)
,再通过散列算法定位到
table(
使用了散列值
的全部
)
。整个
get
过程,没有加锁,而是通过
volatile
保证
get
总是可以拿到最
新值。
![](https://i-blog.csdnimg.cn/blog_migrate/53fbca55de5d80170e2af6d5dc02ee84.png)
4、put操作
ConcurrentHashMap
初始化的时候会初始化第一个槽
segment[0]
,对于其他
槽,在插入第一个值的时候再进行初始化。
ensureSegment
方法考虑了并发情况,多个线程同时进入初始化同一个槽
segment[k]
,但只要有一个成功就可以了。
![](https://i-blog.csdnimg.cn/blog_migrate/37951eb6bea2e8a34b5213f5ccd31fa7.png)
![](https://i-blog.csdnimg.cn/blog_migrate/8477b0e682c2761a4be04e22156f77d6.png)
put
方法会通过
tryLock()
方法尝试获得锁,获得了锁,
node
为
null
进入
try
语句块,没有获得锁,调用
scanAndLockForPut
方法自旋等待获得锁。
![](https://i-blog.csdnimg.cn/blog_migrate/e7ab88260402e69077dff1f58c28c645.png)
scanAndLockForPut
方法里在尝试获得锁的过程中会对对应
hashcode
的链表
进行遍历,如果遍历完毕仍然找不到与
key
相同的
HashEntry
节点,则为后续的
put
操作提前创建一个
HashEntry
。当
tryLock
一定次数后仍无法获得锁,则通过
lock
申请锁。
在获得锁之后,
Segment
对链表进行遍历,如果某个
HashEntry
节点具有相
同的
key
,则更新该
HashEntry
的
value
值。
![](https://i-blog.csdnimg.cn/blog_migrate/3f9328f916790f26dcc2cdc9d7911aee.png)
否则新建一个
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
中的分布是这样的
扩容后
table
长度变为
8
,那么元素在
table
中的分布变成:
![](https://i-blog.csdnimg.cn/blog_migrate/c5132b159c4e2c8876c37a315d30cee9.png)
可以看见
hash
值为
34
和
56
的下标保持不变,而
15,23,77
的下标都是在原
来下标的基础上
+4
即可,可以快速定位和减少重排次数。
该方法没有考虑并发,因为执行该方法之前已经获取了锁。
6、remove操作
与
put
方法类似,都是在操作前需要拿到锁,以保证操作的线程安全性。
7、ConcurrentHashMap 的弱一致性
然后对链表遍历判断是否存在
key
相同的节点以及获得该节点的
value
。但
由于遍历过程中其他线程可能对链表结构做了调整,因此
get
和
containsKey
返
回的可能是过时的数据,这一点是
ConcurrentHashMap
在弱一致性上的体现。如
果要求强一致性,那么必须使用
Collections.synchronizedMap()
方法。
8、size、containsValue
这些方法都是基于整个
ConcurrentHashMap
来进行操作的,他们的原理也基
本类似:首先不加锁循环执行以下操作:循环所有的
Segment
,获得对应的值以
及所有
Segment
的
modcount
之和。在
put
、
remove
和
clean
方法里操作元素
前都会将变量
modCount
进行变动,如果连续两次所有
Segment
的
modcount
和相等,则过程中没有发生其他线程修改
ConcurrentHashMap
的情况,返回获得
的值。
当循环次数超过预定义的值时,这时需要对所有的
Segment
依次进行加锁,
获取返回值后再依次解锁。所以一般来说,应该避免在多线程环境下使用
size
和
containsValue
方法。