ConcurrentHashMap使用了分段锁技术,每个Segment都是一把锁,本身Segment继承了ReentranLock
ConcurrentHashMap由 Segment 数组、每个Segment包含一个HashEntry 数组(HashEntry<K,V>[] table),为一个小的hash表。 Segment和 HashMap 一样,仍然是数组加链表。
- put总结:
put只对对应的Segment加锁,其他的Segment不受影响,提高并发度。
就是说如果容量大小是16他的并发度就是16,可以同时允许16个线程操作16个Segment而且还是线程安全的。
1)根据散列码hash( 调用segmentFor(hash) )找到对应的 Segment
2)在这个 Segment 中执行具体的真正的 put 操作(segment.put( key, hash, value, false) )
3)加锁。对该Segment 对象而不是整个concurrentHashMap加锁
4)得到该散列码对应的 table 数组的下标值index,并找到散列码对应的具体的那个桶 tab[index]
5)while寻找,若键 / 值对已存在,覆盖新值返回旧值; 键 / 值对不存在 ,创建新节点,并添加到链表的头部 ,返回 null
6)解锁该Segment
//并非完整源码,只是关键部分
public V put(K key, V value) {
if (value == null) //ConcurrentHashMap 中不允许用 null 作为映射值
throw new NullPointerException();
int hash = hash(key.hashCode()); // 计算键对应的散列码
// 根据散列码找到对应的 Segment
return segmentFor(hash).put(key, hash, value, false);
}
根据 hash 值找到对应的 Segment:
/**
* 使用 key 的散列码来得到 segments 数组中对应的 Segment
*/
final Segment<K,V> segmentFor(int hash) {
// 将散列值右移 segmentShift 个位,并在高位填充 0
// 然后把得到的值与 segmentMask 相“与”
// 从而得到 hash 值对应的 segments 数组的下标值
// 最后根据下标值返回散列码对应的 Segment 对象
return segments[(hash >>> segmentShift) & segmentMask];
}
V put(K key, int hash, V value, boolean onlyIfAbsent) {
lock(); // 加锁,这里是锁定某个 Segment 对象而非整个 ConcurrentHashMap
try {
int c = count;
if (c++ > threshold) // 如果超过再散列的阈值
rehash(); // 执行再散列,table 数组的长度将扩充一倍
HashEntry<K,V>[] tab = table;
// 把散列码值与 table 数组的长度减 1 的值相“与”
// 得到该散列码对应的 table 数组的下标值
int index = hash & (tab.length - 1);
// 找到散列码对应的具体的那个桶
HashEntry<K,V> first = tab[index];
HashEntry<K,V> e = first;
while (e != null && (e.hash != hash || !key.equals(e.key)))
e = e.next;
V oldValue;
if (e != null) { // 如果键 / 值对以经存在
oldValue = e.value;
if (!onlyIfAbsent)
e.value = value; // 设置 value 值
}
else { // 键 / 值对不存在
oldValue = null;
++modCount; // 要添加新节点到链表中,所以 modCont 要加 1
// 创建新节点,并添加到链表的头部
tab[index] = new HashEntry<K,V>(key, hash, first, value);
count = c; // 写 count 变量
}
return oldValue;
} finally {
unlock(); // 解锁
}
`}
- get总结:
在该Segment内部get。get全程不加锁,并发度高。
get操作可以无锁是由于HashEntry的元素val和指针next是用volatile修饰的,在多线程环境下线程A修改结点的val或者新增节点的时候是对线程B可见的。
1)key 通过 hash 之后定位到具体的 Segment,再进行get操作。
2)count不为0,使用hash得到对应桶的第一个HashEntry
注:count为在本 segment 范围内,包含的 HashEntry 元素的个数。该变量被声明为 volatile 型,保证每次读取到最新的数据
3)while循环依次遍历,使用if(e.hash == hash && key.equals(e.key)) 寻找目标值。
V get(Object key, int hash) {
if(count != 0) { // 首先读 count 变量
HashEntry<K,V> e = getFirst(hash);
while(e != null) {
if(e.hash == hash && key.equals(e.key)) {
V v = e.value;
if(v != null)
return v;
// 如果读到 value 域为 null,说明发生了重排序,加锁后重新读取
return readValueUnderLock(e);
}
e = e.next;
}
}
return null;
}
V readValueUnderLock(HashEntry<K,V> e) {
lock();
try {
return e.value;
} finally {
unlock();
}
}
是的,因为基本上还是数组加链表的方式,我们去查询的时候,还得遍历链表,会导致效率很低,这个跟jdk1.7的HashMap是存在的一样问题,所以他在jdk1.8完全优化了。其中抛弃了原有的 Segment 分段锁,而采用了 CAS + synchronized
来保证并发安全性。