ConcurrentHashMap是jdk1.5之后支持高并发、高吞吐量的线程安全的HashMap实现。Hashtable虽然使用synchronized保证线程安全,但它是锁住整张hash表让单一线程独占,非常影响性能。ConcurrentHashMap也需要通过加锁保证线程安全,但其锁的粒度及如何加锁的方法使其能够支持高并发、高吞吐量并保证线程安全。
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;
}
如上可见,ConcurrentHashMap读取并发的时候,由于其他都设置为final,只有value设置为volatile可以防止链表被破坏,确保读操作可以看到最新的值而避免加锁。
static final class Segment<K,V> extends ReentrantLock implements Serializable {
private static final long serialVersionUID = 2249069246763182397L;
transient volatile int count;
transient int modCount;
transient int threshold;
transient volatile HashEntry<K,V>[] table;
final float loadFactor;
Segment(int initialCapacity, float lf) {
loadFactor = lf;
setTable(HashEntry.<K,V>newArray(initialCapacity));
}
Segment里面的成员变量的意义:
count:Segment中元素的数量
modCount:对table的大小造成影响的操作的数量(比如put或者remove操作)
threshold:阈值,Segment里面元素的数量超过这个值依旧就会对Segment进行扩容
table:链表数组,数组中的每一个元素代表了一个链表的头部
loadFactor:负载因子,用于确定threshold
ConcurrentHashMap将hash表分为多个段(默认为16个),做修改操作的时候每个线程只需要锁定所用到的段,多个修改操作修改不同的段时,并发性明显提升。通常只有在跨段的操作(size()、containsValue()等)需要按顺序锁住所有段,完成后再按顺序释放所有段。
在迭代的时候,ConcurrentHashMap使用了weakly consistent迭代器。迭代器初始化后,若有线程修改ConcurrentHashMap,会copy一个新的ConcurrentHashMap用于修改,在iterator完成之后再将头指针替换为新的数据使得迭代和修改同时进行。
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();
}
}
remove删除操作先定位到段,然后委托给段的remove操作,不同段的remove操作可同时并发进行。两点注意:第一、删除最后一步操作是将count的值减1,必须最后做此操作,否则读取操作可能看不到之前对段所做的结构性修改;第二、remove执行开始将table赋给一个局部变量tab,因为读volatile变量开销很大,编译器也不能对volatile变量的读写做任何优化。
Put操作也是委托给段的put方法,判断是否需要rehash,是否存在同样一个key的结点,如果存在就直接替换这个结点的值。否则创建一个新的结点并添加到hash链的头部,这时一定要修改modCount和count的值,同样修改count的值一定要放在最后一步。
Size()方法是先在没有锁的情况下对所有段大小求和,如果不成功(遍历时有其他线程对已经遍历过的段进行结构性更新),最多执行RETRIES-BEFORE-LOCK次,还不成功就在持有段锁的情况下对所有段大小求和。没有锁的情况下主要是利用segment的modcount进行检测 ,遍历过程中保存每个segment的modcount,遍历完之后再检测每个modcount有没有改变,有改变就重新计算。