- 简介
- 源码分析
- ConcurrentMap#putIfAbsent()
- ConcurrentMap#remove()
- ConcurrentMap#replace(K key, V oldValue, V newValue)
- ConcurrentMap#replace(K key, V value)
- ConcurrentHashMap属性
- ConcurrentHashMap#Segment<K,V>
- ConcurrentHashMap#Segment<K,V>属性
- ConcurrentHashMap#Segment<K,V>构造
- ConcurrentHashMap#Segment<K,V>#setTable
- ConcurrentHashMap#HashEntry
- ConcurrentHashMap#Constants
- ConcurrentHashMap构造方法
- ConcurrentHashMap#put()
- ConcurrentHashMap#hash()
- ConcurrentHashMap#segmentFor()
- ConcurrentHashMap#Segment#put()
- ConcurrentHashMap#Segment#rehash()
- ConcurrentHashMap#get()
- ConcurrentHashMap#Segment#get()
- ConcurrentHashMap#Segment#getFirst()
- ConcurrentHashMap#Segment#readValueUnderLock()
- ConcurrentHashMap#Segment#remove()
- ConcurrentHashMap#size()
- 总结
简介
ConcurrentHashMap是一种线程安全的HashMap。相对于HashTable和Collections.synchronizedMap(),ConcurrentHashMap具有更好的性能和伸缩性,是由于其使用了分段锁的策略,将内部数据分为多个段,每个段单独加锁,而不是整个HashMap加锁,这样能减少很多不必要的锁争用。
源码分析
类结构图
ConcurrentMap#putIfAbsent()
1 | /** |
如果map中已经存在给定的key,返回map中key对应的value;如果不存在给定的key,插入给定的key和value。
这个是一个原子操作,逻辑相当于如下代码。1
2
3
4if (!map.containsKey(key))
return map.put(key, value);
else
return map.get(key);
ConcurrentMap#remove()
1 | /** |
如果map中存在给定的key,并且map中对应的value等于给定的value,那么删除这个key和value。
这是一个原子操作,逻辑相当于如下代码。1
2
3
4if (map.containsKey(key) && map.get(key).equals(value)) {
map.remove(key);
return true;
} else return false;
ConcurrentMap#replace(K key, V oldValue, V newValue)
1 | /** |
如果map中存在给定的key,并且map中对应的value也等于给定的oldValue,那么将这个key对应的value替换成newValue。
这是一个原子操作,逻辑相当于如下代码。1
2
3
4if (map.containsKey(key) && map.get(key).equals(oldValue)) {
map.put(key, newValue);
return true;
} else return false;
ConcurrentMap#replace(K key, V value)
1 | /** |
如果map中已经存在给定的key,那么将这个key对应的value替换成给定的value。
这是一个原子操作,逻辑相当于如下代码。1
2
3if (map.containsKey(key)) {
return map.put(key, value);
} else return null;
ConcurrentHashMap属性
1 | /** |
标注代码分析
- 用于key的hash code计算,在segment数组中选择合适的segment。
- segment#hash索引的偏移量。
- segment是一个特殊的hash table。
ConcurrentHashMap#Segment<K,V>
Segment结构图
ConcurrentHashMap#Segment<K,V>属性
1 | /** |
标注代码分析
- 记录segment中的元素数量。其他操作会利用count的volatile读写来保证可见性,避免使用锁。
- 统计跟踪修改,用来保证一些批量操作的一致性。如果modCount计算siez()或检查value的遍历过程中发生变化,那么可能会有一个不一致的状态,必须重新检测状态。
- 当哈希表的容量超过这个阀值会扩容,里面的元素会重新散列。 capacity * loadFactor
- 哈希表的加载因子。
ConcurrentHashMap#Segment<K,V>构造
1 | Segment(int initialCapacity, float lf) { |
初始化加载因子,初始化HashEntry数组,数组容量为initialCapacity。
哈希表内部一般会有初始容量size和加载因子loadFactor,当哈希表中的元素数量达到(size * loadFactor)的时候,就会触发哈希表进行rehash()。假设哈希表使用链表法来解决哈希冲突,那么如果加载因子太大,会导致哈希表中每个桶里面的链表平均长度过长,这样会影响查询性能;但如果加载因子过小,又会浪费太多内存空间。时间和空间的权衡,需要按实际情况来选择合适的加载因子。
ConcurrentHashMap#Segment<K,V>#setTable
1 | /** |
初始化threshold、HashEntry<K,V>[] table。
ConcurrentHashMap#HashEntry
1 | static final class HashEntry<K,V> { |
HashEntry#newArray()生成HashEntry数组,数组容量是i。volatile V value
是volatile根据Java Menory Mode的Happen-Before保证可见性。
ConcurrentHashMap#Constants
1 | /** |
标注代码分析
- 默认capacity值,segment中hashTable长度。
- 默认加载因子。
- 默认Segment#table的并发级别,影响Segment#table容量。
- Segment#table的最大容量。
- 允许的最大的Segment#table容量。
- 在size()和containsValue(),加锁之前的尝试操作次数。
ConcurrentHashMap构造方法
1 | public ConcurrentHashMap(int initialCapacity, |
标注代码分析
- ssize最后是比concurrencyLevel大的最小的2的幂。如果concurrencyLevel是50,那么ssize是64,segmentShift是26,segmentMask是 00000000 00000000 00000000 00111111。
- cap是比总体容量平均分到每个segment的数量大的最小的2的幂。
- 初始化segments[]。
ConcurrentMap内部包含一个segment的数组;而segment本身又是一个哈希表(HashEntry<K,V>[] table),并且自带锁(继承ReentrantLock);内部哈希表使用链表法解决哈希冲突,每个数组元素是一个单链表(HashEntry)。
ConcurrentHashMap#put()
1 | public V put(K key, V value) { |
标注代码分析
- 重新计算hash值,根据hash值确认segment。
- 执行segment#put。
ConcurrentHashMap#hash()
1 | private static int hash(int h) { |
key#hashCode再次hash一次,使得hash值更加散列。因为ConcurrentHashMap中哈希表的长度都是2的幂,会增加一些冲突几率,比如两个hashCode高位不同但低位相同,对哈希表长度取模时正好忽略了这些高位,造成冲突。这里是采用了Wang/Jenkins
哈希算法的一个变种。
ConcurrentHashMap#segmentFor()
1 | /** |
& segmentMask除去高位确定segments下标。
ConcurrentHashMap#Segment#put()
1 | V put(K key, int hash, V value, boolean onlyIfAbsent) { |
标注代码分析
- Segment继承ReentrantLock,put()加锁。锁是Segment对象上。
- 超过扩容阀值,那么进行rehash(),扩容。
- hash & (tab.length - 1)取模高效一种方式。获取链表HashEntry下标。
- 遍历链表,判断key是否相同,找到系统的key,结束循环或者e = null。
- 链表HashEntry#key与key相同,判断是否覆盖旧value。
- 没有找到链表HashEntry#key与key相同,则新增一个节点,modCount作为ConcurrentHashMap#size()、ConcurrentHashMap#isEmpty()、、ConcurrentHashMap#containsValue()重新检测segments数组状态。
- 在HashEntry[index]上HashEntry新建1个节点。
- 操作执行成功,保证volatile的写可见性。
- 解锁。
ConcurrentHashMap#Segment#rehash()
1 | void rehash() { |
标注代码分析
- 获取Segment#table的HashEntry<K,V>对象,对象不存在,直接返回newTable。
- e#next对象是否存在,如果不存在,赋值当前e对象到newTable新位置。newTable新位置下标是根据e.hash & sizeMask计算出来。
- HashEntry e#next存在,假设当前e和e的下标作为newTable的最后一个链表HashEntry对象。
- for循环寻找节点e#next的后续节点,寻找最后1个HashEntry对象以及下标。
- 确定newTable数组下标边界,且赋值最后1个newTable数组对象HashEntry。
- 从e对象循环查询到最后1个对象last,从而克隆原table数据到newTable。
ConcurrentHashMap#get()
1 | public V get(Object key) { |
ConcurrentHashMap#Segment#get()
1 | V get(Object key, int hash) { |
标注代码分析
- volatile的读可见性。
- 根据hash获取正确的链表HashEntry对象e。
- 判断当前e#hash和e#key是否一致。一致则返回e#value。
- 读取操作加锁,e#value=null有可能是在Segment#table的HashEntry初始化时候发生。
- 如果e不匹配,寻找e#next。
ConcurrentHashMap#Segment#getFirst()
1 | /** |
返回Segment#table数组HashEntry对象。
ConcurrentHashMap#Segment#readValueUnderLock()
1 | /** |
根据注释可以理解,e#value是null,可能发生在HashEntry初始化阶段。
ConcurrentHashMap#Segment#remove()
1 | V remove(Object key, int hash, Object value) { |
1 | 情况1 |
标注代码分析
- 分为2种情况。情况1,while循环第1次就判断e正确,e=first,且e=frist=p,不执行for循环。e#index替换e原来的位置tab[index]。情况2,while循环第1次未找到正确e,e=e#next之后e!=first,如代码图e的新位置是e1,p=first,p!=e1,新建HashEntry对象,key、value、hash都是p,但是next是e1#next
p!=e1,就循环的把p-e1之间差额,新建HashEntry,同时把next指向e1#next。直到p=e1时候退出循环,这时候newFirst是e1的上一个节点。替换e1的位置也就是index。 - e上一个节点HashEntry替换e在tab的位置index。
ConcurrentHashMap#size()
1 | /** |
标注代码分析
- sum是所有segment#HashEntry#count总和。check是segment#HashEntry有变化时候的count总和。mc是segment#HashEntry变化数字。
- 执行2次无锁检查。新建mcsum属性,segment#HashEntry变化数字总和。
- 循环segment数组,计算sum、mcsum、mc。
- 如果mcsum!=0,说明有segment#HashEntry数据变化,计算check。如果mc[]!=segment[]#modcount,segment#HashEntry数据又变化(modCount在ConcurrentHashMap#put()、ConcurrentHashMap#remove()、ConcurrentHashMap#clear()才会发生变化),结束本次for循环,再执行for循环后面计算无意义。
- 如果2次计算count一致,那么数量就一致,返回count。
- 2次无锁统计count不一致,segment[]每个HashEntry对象都加锁,执行count统计。
总结
count
All (synchronized) write operations should write to the “count” field after structurally changing any bin.
bin
是HashEntry,HashEntry结构发生变化(添加或者删除),才会写count,保证count可见性。
HashEntry属性中value是volatile,所以本身就保证可见性。覆盖value(、ConcurrentHashMap#replace())时候,不需要重写count。
lock()
锁粒度在segment[]的HashEntry上,所以可以保证segment可以并发操作。