经研究过jkd1.5新特性,其中ConcurrentHashMap就是其中之一,其特点:效率比Hashtable高,并发性比hashmap好。结合了两者的特点。
集合是编程中最常用的数据结构。而谈到并发,几乎总是离不开集合这类高级数据结构的支持。比如两个线程需要同时访问一个中间临界区(Queue),比如常会用缓存作为外部文件的副本(HashMap)。这篇文章主要分析jdk1.5的3种并发集合类型(concurrent,copyonright,queue)中的ConcurrentHashMap,让我们从原理上细致的了解它们,能够让我们在深度项目开发中获益非浅。
ConcurrentHashMap和Hashtable主要区别就是围绕着锁的粒度以及如何锁。如图
左边便是Hashtable的实现方式---锁整个hash表;而右边则是ConcurrentHashMap的实现方式---锁桶(或段segment)。 ConcurrentHashMap将hash表分为16个桶(默认值),诸如get,put,remove等常用操作只锁当前需要用到的桶。试想,原来 只能一个线程进入,现在却能同时16个写线程进入(写线程才需要锁定,而读线程几乎不受限制,之后会提到),并发性的提升是显而易见的。其实ConcurrentHashMap的设计原理跟HashMap差不多,都是通过哈希表来维护数据,只是hashmap用一张哈希表维护数据而concurrenthashmap是俩张哈希表来维护数据的,它首先通过key的哈希值计算出目标元素在哪个segment中,找到对应的segment时再对次表加锁,然后哈希计算得出次表索引值,最后在数据链上找到目标元素做同步修改操作。
下面介绍下ConcurrentHashMap在get、put、remove操作数据时如何保持同步。
get操作
----------------concurrmenthashmap方法---------
public V get(Object key) {
int hash = hash(key.hashCode());
return segmentFor(hash).get(key, hash);
}
//对主表求索引
final Segment<K,V> segmentFor(int hash) {
return segments[(hash >>> segmentShift) & segmentMask]; }--------------segment方法-----------------------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; }//对次表求索引
HashEntry<K,V> getFirst(int hash) {
HashEntry<K,V>[] tab = table;
return tab[hash & (tab.length - 1)];
}
上面正好对应着前面所说的先求主表索引,再求次表索引。这里详细讲解下segment中的get操作,在找到目标节点时,如果目标节点对应的value不为null时,则直接返回,否则的话就说明在并发环境下,该节点的value值可能已经被其他线程修改,还没来得及写回就被读取,这时就调用readValueUnderLock方法
V readValueUnderLock(HashEntry<K,V> e) {
lock();
try {
return e.value;
} finally {
unlock();
}
}
对该节点进行同步读取。
Size方法
public int size() {
final Segment<K,V>[] segments = this.segments;
long sum = 0; //保存所有段中节点数的和
long check = 0; //检查在执行size方法期间是否有其他线程删除或添加节点元素,如果check==sum则
//说明期间没有修改,数据正确,否则说明有结构修改,重新加锁计算
int[] mc = new int[segments.length];
//尝试RETRIES_BEFORE_LOCK次计算节点总数,由于是并发环境下,很可能在计算期间节点数目被其他线程修改导致
//数目不正确,因此循环计算多次,如果还是不正确则采用表加锁计算
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) { //如果数据还不正确,使用加锁同步计算
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;
}
put操作
public V put(K key, V value) {
if (value == null)
throw new NullPointerException();
int hash = hash(key.hashCode());
return segmentFor(hash).put(key, hash, value, false);//主表hash计算求索引
}
V put(K key, int hash, V value, boolean onlyIfAbsent) {
lock(); //加锁同步
try {
int c = count;
if (c++ > threshold)
rehash(); //如果超过容量,2倍扩容,并且从新计算hash值
HashEntry<K,V>[] tab = table;
int index = hash & (tab.length - 1); //次表hash计算求索引
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();
}
}
remove操作
-------------concurrenthashmap方法----------------------
public V remove(Object key) {
int hash = hash(key.hashCode());
return segmentFor(hash).remove(key, hash, null);//调用segment的remove方法
}
--------------segment方法--------------------------------
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();
}
}
通过上面的代码可以看出concurrenthashmap只对put、remove等修改数据结构的操作做严格的同步限制,而对于get、size等只读操作只在需要的时候做同步。concurrenthashmap之所以在并发环境下比hashtable效率更高,是因为它设计了主、次表,通过在更小粒度上给segement的HashEntry[] table加锁,而不影响其他线程对其他segment的操作,从而提高效率。