ConcurrentHashMap是JUC中一个高性能的线程安全的hash表,key和value都不可为null,ConcurrentHashMap通过如下手段来来达到它的高性能:
1、分离写锁:普通的Hash表通过桶数组+冲突链表来实现,ConcurrentHashMap增加了一个段结构(Segment),通过段数组+桶数组+冲突链表三层结构来实现,在进行put操作的时候先通过hash码定位到对应的段,然后在段上加锁,这样的话如果多个线程的写操作在不同的段上执行,那么这些线程的写操作不会相互阻塞,通过这种把锁从全局锁细化到段锁的手段来增加put操作的并发性。
2、读操作(get)无锁,那么ConcurrentHashMap是如何在不加锁的情况下保证数据的一致性的呢?这块的实现是ConcurrentHashMap最精妙的地方,来看看它的代码:
ConcurrentHashMap$Segment
V put(K key, int hash, V value, boolean onlyIfAbsent) {
lock();
try {
int c = count;
if (c++ > threshold) // ensure capacity
rehash();
HashEntry<K,V>[] tab = 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;
}
else {
oldValue = null;
++modCount;
tab[index] = new HashEntry<K,V>(key, hash, first, value);
count = c; // write-volatile
}
return oldValue;
} finally {
unlock();
}
}
V get(Object key, int hash) {
if (count != 0) { // read-volatile
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;
return readValueUnderLock(e); // recheck
}
e = e.next;
}
}
return null;
}
在Segment中有个关键的变量count,它是一个volatile修饰的变量,它起着非常重要的作用,当put的key值在hash段中不存在(暂时先看着一个分支)时,put操作完成之后把当前的元素个数设置给了count变量,在get操作的最开始读取了count变量的值,在JMM规范中对volatile变量的描述:volatile变量的写happens-before对volatile变量的读,而hb又有传递性,所以volatile变量的写之前的操作hb读volatile变量之后的操作,所以只要线程的put操作在其它线程的get操作之前完成,那么put操作后的数据对get操作时可见的,这样就达到了数据的一致性,来看另一个分支,当key在段中已经存在时直接把value赋值给Entry的value字段,而Entry的value字段也是volatile字段,所以也能保证数据更新后的可见性。但是这种实现方式无法保证强一致性,只能保证弱一致性:
put | |
get | |
get返回 | |
put完成 |
上面代码只是实现了put和get之间如何达到数据一致性,我们知道在hash表中删除操作也可以修改数据,那么remove操作和get之间如何实现一致性呢,来看看代码:
static final class HashEntry<K,V> {
final K key;
final int hash;
volatile V value;
final HashEntry<K,V> next;
HashEntry(K key, int hash, HashEntry<K,V> next, V value) {
this.key = key;
this.hash = hash;
this.next = next;
this.value = value;
}
@SuppressWarnings("unchecked")
static final <K,V> HashEntry<K,V>[] newArray(int i) {
return new HashEntry[i];
}
}
V remove(Object key, int hash, Object value) {
lock();
try {
int c = count - 1;
HashEntry<K,V>[] tab = 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 = null;
if (e != null) {
V v = e.value;
if (value == null || value.equals(v)) {
oldValue = v;
// All entries following removed node can stay
// in list, but all preceding ones need to be
// cloned.
++modCount;
HashEntry<K,V> newFirst = e.next;
for (HashEntry<K,V> p = first; p != e; p = p.next)
newFirst = new HashEntry<K,V>(p.key, p.hash,
newFirst, p.value);
tab[index] = newFirst;
count = c; // write-volatile
}
}
return oldValue;
} finally {
unlock();
}
}
从代码中可以看到,在HashEntry结构中,除了value字段其它字段都是不可变的,在删除时,找到待删除的节点,把该节点之前的所有节点都复制,最后一个节点的next指针指向删除节点的后一个节点。这种方式也只能保证弱一致性(事实证明强一致性和性能两者只能取其一)。
因为ConcurrentHashMap时分段结构,所以对于size和containValues这种全局操作需要扫描所有段,所以为了保证数据一致性必须要加全局锁,对于这两个方法ConcurrentHashMap也做了优化:在加锁之前先尝试几次,比如size方法,先在不加锁的情况下扫描所有段,统计所有段的元素个数,并且统计modCount,如果modCount不为0,那么再检查一次,统计所有段的count并且比较之前记录各个段的modCount,一段modCount发生了变化或者前后两次统计的所有段的元素数量不一致,说明在统计的过程中hash表中有某些的段的数据发生了变化,那么这时候给所有段加锁重新扫描一次,containsValue的原理类似。
public int size() {
final Segment<K,V>[] segments = this.segments;
long sum = 0;
long check = 0;
int[] mc = new int[segments.length];
// Try a few times to get accurate count. On failure due to
// continuous async changes in table, resort to locking.
for (int k = 0; k < RETRIES_BEFORE_LOCK; ++k) {
check = 0;
sum = 0;
int mcsum = 0;
for (int i = 0; i < segments.length; ++i) {
sum += segments[i].count;
mcsum += mc[i] = segments[i].modCount;
}
if (mcsum != 0) {
for (int i = 0; i < segments.length; ++i) {
check += segments[i].count;
if (mc[i] != segments[i].modCount) {
check = -1; // force retry
break;
}
}
}
if (check == sum)
break;
}
if (check != sum) { // Resort to locking all segments
sum = 0;
for (int i = 0; i < segments.length; ++i)
segments[i].lock();
for (int i = 0; i < segments.length; ++i)
sum += segments[i].count;
for (int i = 0; i < segments.length; ++i)
segments[i].unlock();
}
if (sum > Integer.MAX_VALUE)
return Integer.MAX_VALUE;
else
return (int)sum;
}
以上代码都是基于JDK6的实现,ConcurrentHashMap在JDK7中的实现有比较大的变化,这个放到另外的文章分析。