Learn && Live
虚度年华浮萍于世,勤学善思至死不渝
前言
Hey,欢迎阅读Connor学Java系列,这个系列记录了我的Java基础知识学习、复盘过程,欢迎各位大佬阅读斧正!原创不易,转载请注明出处:http://t.csdn.cn/vMvcI,话不多说我们马上开始!
JDK7中的ConcurrentHashMap是如何保证线程安全的?
分段锁技术
public class ConcurrentHashMap<K, V>
extends AbstractMap<K, V>
implements ConcurrentMap<K, V>, Serializable {
final Segment<K,V>[] segments;
static final class Segment<K,V>
extends ReentrantLock
implements Serializable {
transient volatile HashEntry<K,V>[] table;
transient int count;
}
// 基本节点,存储Key, Value值
static final class HashEntry<K,V> {
final int hash;
final K key;
volatile V value;
volatile HashEntry<K,V> next;
}
}
分段锁的思想就是:锁的时候不锁整个hash表,而是只锁一部分。
如何实现呢?这就用到了ConcurrentHashMap中最关键的Segment。
ConcurrentHashMap中维护着一个Segment数组,每个Segment可以看做是一个HashMap。
而Segment本身继承了ReentrantLock,它本身就是一个可重入锁。
在Segment中通过HashEntry数组来维护其内部的hash表。
每个HashEntry就代表了map中的一个K-V,用HashEntry可以组成一个链表结构,通过next字段引用到其下一个元素。
所以JDK7中ConcurrentHashMap的整体结构可以描述为下图:
put方法的线程安全
根据上述结构很容易想到,ConcurrentHashMap通过哈希值定位一个元素需要两次,第一次定位到Segment,第二次才是我们熟悉的HashMap定位到某一链表的头部。
ConcurrentHashMap的put方法源码如下:
public V put(K key, V value) {
Segment<K,V> s;
if (value == null)
throw new NullPointerException();
// (1)
int hash = hash(key);
int j = (hash >>> segmentShift) & segmentMask;
// (2)
if ((s = (Segment<K,V>)UNSAFE.getObject // nonvolatile; recheck
(segments, (j << SSHIFT) + SBASE)) == null) // in ensureSegment
s = ensureSegment(j);
// (3)
return s.put(key, hash, value, false);
}
(1)计算key的哈希值
(2)根据key的哈希值定位一个Segment,懒加载:若Segment还未初始化,则调用ensureSegment初始化
(3)调用Segment的put方法
下面来看Segment内的put方法:
static final class Segment<K,V>
extends ReentrantLock
implements Serializable {
final V put(K key, int hash, V value, boolean onlyIfAbsent) {
HashEntry<K,V> node = tryLock() ? null : scanAndLockForPut(key, hash, value);
V oldValue;
...
}
private HashEntry<K,V> scanAndLockForPut(K key, int hash, V value) {
HashEntry<K,V> first = entryForHash(this, hash);
HashEntry<K,V> e = first;
HashEntry<K,V> node = null;
int retries = -1; // negative while locating node
// tryLock尝试获取锁,重复64次
while (!tryLock()) {
HashEntry<K,V> f; // to recheck first below
if (retries < 0) {
if (e == null) {
if (node == null) // speculatively create node
node = new HashEntry<K,V>(hash, key, value, null);
retries = 0;
}
else if (key.equals(e.key))
retries = 0;
else
e = e.next;
}
// 重试超过64次,lock()阻塞线程直至获取到锁
else if (++retries > MAX_SCAN_RETRIES) {
lock();
break;
}
else if ((retries & 1) == 0 && (f = entryForHash(this, hash)) != first) {
e = first = f; // re-traverse if entry changed
retries = -1;
}
}
return node;
}
}
(1)因为Segment本身就是一个锁,所以这里调用tryLock尝试获取锁
-
如果获取成功,那么其他线程都无法再修改这个segment
-
如果获取失败,会调用scanAndLockForPut方法根据key和hash尝试找到这个node,如果不存在,则创建一个node并返回,如果存在则返回null
(2)查看scanAndLockForPut源码会发现在查找的过程中会尝试获取锁,在多核CPU环境下,会尝试64次tryLock(),如果64次还没获取到,会直接调用lock(),也就是说这一步一定会获取到锁
rehash扩容的线程安全
HashMap的线程安全问题大部分出在扩容(rehash)的过程中。
ConcurrentHashMap的扩容只针对每个segment中的HashEntry数组进行扩容。
由上述put的源码可知,ConcurrentHashMap在rehash的时候是有锁的,所以在rehash的过程中,其他线程无法对segment的hash表做操作,这就保证了线程安全。