ConcurrentHashMap JDK1.7实现原理(一)

本文目录索引:

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 锁。
 

2、构造方法和初始化

ConcurrentHashMap 初始化方法是通过 initialCapacityloadFactor

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 操作

get 操作先经过一次再散列,然后使用这个散列值通过散列运算定位到
Segment( 使用了散列值的高位部分 ) ,再通过散列算法定位到 table( 使用了散列值
的全部 ) 。整个 get 过程,没有加锁,而是通过 volatile 保证 get 总是可以拿到最
新值。

4、put操作

ConcurrentHashMap 初始化的时候会初始化第一个槽 segment[0] ,对于其他
槽,在插入第一个值的时候再进行初始化。
ensureSegment 方法考虑了并发情况,多个线程同时进入初始化同一个槽
segment[k] ,但只要有一个成功就可以了。
put 方法会通过 tryLock() 方法尝试获得锁,获得了锁, node null 进入 try
语句块,没有获得锁,调用 scanAndLockForPut 方法自旋等待获得锁。
 
scanAndLockForPut 方法里在尝试获得锁的过程中会对对应 hashcode 的链表
进行遍历,如果遍历完毕仍然找不到与 key 相同的 HashEntry 节点,则为后续的
put 操作提前创建一个 HashEntry 。当 tryLock 一定次数后仍无法获得锁,则通过
lock 申请锁。

在获得锁之后, Segment 对链表进行遍历,如果某个 HashEntry 节点具有相
同的 key ,则更新该 HashEntry value 值。
 
否则新建一个 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 中的分布变成:
 
可以看见 hash 值为 34 56 的下标保持不变,而 15,23,77 的下标都是在原
来下标的基础上 +4 即可,可以快速定位和减少重排次数。
该方法没有考虑并发,因为执行该方法之前已经获取了锁。

6、remove操作

put 方法类似,都是在操作前需要拿到锁,以保证操作的线程安全性。

7、ConcurrentHashMap 的弱一致性

然后对链表遍历判断是否存在 key 相同的节点以及获得该节点的 value 。但
由于遍历过程中其他线程可能对链表结构做了调整,因此 get containsKey
回的可能是过时的数据,这一点是 ConcurrentHashMap 在弱一致性上的体现。如
果要求强一致性,那么必须使用 Collections.synchronizedMap() 方法。

8、sizecontainsValue

这些方法都是基于整个 ConcurrentHashMap 来进行操作的,他们的原理也基
本类似:首先不加锁循环执行以下操作:循环所有的 Segment ,获得对应的值以
及所有 Segment modcount 之和。在 put remove clean 方法里操作元素
前都会将变量 modCount 进行变动,如果连续两次所有 Segment modcount
和相等,则过程中没有发生其他线程修改 ConcurrentHashMap 的情况,返回获得
的值。
 
当循环次数超过预定义的值时,这时需要对所有的 Segment 依次进行加锁,
获取返回值后再依次解锁。所以一般来说,应该避免在多线程环境下使用 size
containsValue 方法。
 
  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值