ConcurrentHashMap源码解析

jdk1.7 解析

HashMap在多线程环境下,put操作可能产生死循环,Hashtable是对读写加锁,独占式,一个线程在读时其他线程必须等待,性能低下。现在我们可以使用高性能的线程安全的ConcurrentHashMap。

ConcurrentHashMap采用分段锁的机制,实现并发的更新操作,底层采用数组加链表的存储结构。其包含两个核心静态内部类SegmentHashEntry。Segment继承ReentrantLock用来充当锁的角色,所以每次需要加锁的操作锁住的是一个Segment。这样只要保证每个segment是线程安全的,也就实现了全局的线程安全。Segment内部是一个HashEntry的数组。每个Segment对象其实相当于一个小的HashMap。

 

 

 HashEntry的源码:volatile保证多线程读取时一定是最新值。

 

1、初始化:

InitialCapacity:初始容量,指的是ConcurrentHashMap的初始容量,实际操作的时候需要平均分给每个segment。

LoadFactor:负载因子,由于segment数组从ConcurrentHashMap创建好之后就不会再改变,所以这个负载因子是给每个segment内部使用的。当一个segment中节点数量和数组长度的比值超过加载因子后,会对segment对象的数组进行扩容(数组长度加倍),然后将原数组中的元素插入到新的数组里。这个过程不会影响别的segment对象,只对本segment操作。

concurrencyLevel:并发等级,用来确定segment的数量,segment的个数为大于等于concurrencyLevel的第一个2的n次方。例如此时concurrencyLevel12或13,则此时segment的数量是16.

 

ssize: segment数组长度

segmentMask:是segment数组长度-1,默认为15,主要是为了让低位为1,这样在做&运算确定segment的索引时能够更加分散。

segmentShift:默认为28(32-sshift)

c:是每个segment中存储键值对的个数,也就是HashEntry的个数,cap是segment里HashEntry数组的长度。cap是1或者2的N次方,segment的容量是cap*加载因子。Segment的容量大于这个值就会扩容。

2、put操作

 

1)判断值是否为null

2)计算key的hash值,得到hash值向右按位移动segmentShift位,然后再与segmentMask做&运算得到segment的索引j,例如此时concurrencyLevel等于16,sshift等于4,则segmentShift28,segmentMask为15,hash值是一个32位整数,向右移动28位就变成:0000…00000 xxxx,然后再用这个值与segmentMask做&运算,也就是取最后四位的值,这个值确定segment的索引。然后在segment对应的数组中进行插入或更新操作,这个过程需要加锁,不同的segment由不同的锁来保护。这就是分段锁的应用,减小锁的粒度,提高性能。注意key和value都不能为null。Put操作先判断是否需要扩容,第二步再定位添加元素的位置。

3)调用segment的put方法。

 

 

注释1:获取锁,保证线程安全

注释2:hash值对数组长度取余,定位到在数组中具体的index

注释3:index中拿到链表,遍历HashEntry链表,如果key对应的hash已经存在,则替换。如果没有重复的,则插入到链表头。

注释4:关闭锁。

3、get操作

注释1:根据key算出hash值

注释2:根据算出的hash值,定位segment,然后在该segment对象对应的数组里进行哈希查找操作。里面的一大段东西,大致讲的就是通过hash值定位segment中对应的HashEntry,遍历HashEntry,如果key存在,返回key对应的value,如果不存在,则返回null。定位segment的过程就是获取到key的hash值,右移segmentShift(28)位,与segmentMask(15)&运算。

get方法为什么不加锁呢?因为entry类中的valuevolatile的,能保证多线程的可见性,get只需要读不需要写,所以不用加锁。之所以不会读到过期值,因为java内存模型采取了先行发生原则,对volatile的写入操作先于读操作,即使两个线程同时修改volatile变量get也能拿到最新的值。这是volatile代替锁的经典情景。

 4、获取size:

由于整个ConcurrentHashMap的数据被划分到多个Segment当中,不同的Segment用不同的锁保护,size操作需要获取所有Segment中的entry节点数量。Segment中有个全局变量count,是一个volatile变量,用来统计HashEntry的个数。每次操作做了结构上的改变比如增加或者删除,都会在最后一步改变count的值。

因为是并发操作,在计算size时,可能ConcurrentHashMap还在并发的插入数据。它会采取两种方案解决这个问题。

第一种方案:在不加锁的模式下计算ConcurrentHashMap的size,并且计算两次modCount字段的值(统计entry总量之前计算一次,统计完entry总量再计算一次),put,remove,clean操作都会使modCount1,如果两次modCount字段的值一致,则认为在计算size的过程中没有其他线程修改ConcurrentHashMap,返回获得的值。

第二种方案:如果第一种方案不符合,就会把所有segmentput,remove等方法全部锁住。最后计算ConcurrentHashMap的size返回。最好避免在多线程环境下使用size方法,因为它可能获取所有segment的锁。

注释1:RETIES_BEFORE_LOCK是不变常量,值为2,即尝试两次不锁住segment的方式来统计每个segment的大小,如果在统计的过程中,segment的count发生变化,这时候再加锁统计segment的count。ConcurrentHashMap的keyvalue都不能是null。get和put操作都只关心一个segment

ConcurrentHashMap的kv为什么都不能为null?如果kv允许为null,比如说某个线程A调用了map.get(key)方法,返回value为null的真实情况就是因为这个key 不存在。线程A还是后续调用map.containsKey(key),我们期望的结果是返回false。如果在线程A调用map.get(key)方法之后,调用map.containsKey方法之前,另一个线程B 执行了 map.put(key,null)的操作。那么线程A调用的 map.containsKey方法返回的就是true 了。总结一下就是map.get返回null,map.containsKey返回true的原因可能有两个,a、key对应的value就是null。b、有其他线程并发插入value为null的entry。并发情况下可能会产生歧义。

HashMap的kv为什么可以为null?HashMap在单线程场景下使用,如果map.get返回null,map.containsKey返回true的原因只有一个就是key对应的value为null。不存在并发插入的问题

 

ConcurrentHashMap 1.7和1.8区别

1)1.7采用了segment+hashEntry实现,1.8采用synchronized和CAS操作,锁粒度更小了。1.8也引入红黑树,当数组槽位链表长度大于等于8时,链表转为红黑树
2)put方法:
1.7中put需要定位两次,先通过key的rehash值的高位和segment数组大小取余定位到segment,然后通过key的rehash值和table数组大小取余定位到table中的位置。没获取到segment锁的线程,不能进行put操作,
1.8中根据key的rehash值定位到table的位置,拿到table[i]的首节点first,
如果first为null:通过cas方式把value put进去。
如果first不为null,并且first的hash值为-1,说明其他线程在扩容,参与一起扩容。concurrentHashMap 1.8中引入了多线程并发扩容的机制,即多个线程对原始数组分片,每个线程负责一个分片的数据迁移。提升扩容的效率。
如果first不为null,并且first的hash值不为-1,Synchronized锁住 first节点,判断是链表还是红黑树,遍历插入。
3)计算size
1.7中先采用不加锁的方式,计算两次,如果两次结果一样,说明是正确的,返回。如果两次结果不一样,则把所有segment锁住,重新计算所有segment的Count的和
1.8中有两个重要属性baseCount和counterCells数组,counterCells中的元素是CounterCell,每个CounterCell都有个value,如果counterCells不为空,那么总共的大小就是baseCount与遍历counterCells中元素的value值累加获得的。如果counterCells为空,那么总共大小就是baseCount
baseCount:是volatile修饰的,每次put操作后会修改这个值
CounterCell:当有并发put,CAS修改baseCount失败,会使用到CounterCell这个类,创建一个CounterCell对象,其value值为1,并把CounterCell对象放到counterCells数组中

 

posted @ 2023-02-10 15:58  MarkLeeBYR  阅读(278)  评论(0编辑  收藏  举报