关于ConcurrentHashMap在以前的ConcurrentHashMap原理分析中已经解释了原理,而HashTable其实大抵上只是对HashMap的线程安全的封装,在JDK7与JDK8中HashMap的实现中解释了HashMap的原理。java
至此你应该可以明白,ConcurrentHashMap与HashTable均可以用于多线程的环境,可是当Hashtable的大小增长到必定的时候,性能会急剧降低,由于迭代时须要被锁定很长的时间。由于ConcurrentHashMap引入了分割(segmentation),不论它变得多么大,仅仅须要锁定map的某个部分,而其它的线程不须要等到迭代完成才能访问map。简而言之,在迭代的过程当中,ConcurrentHashMap仅仅锁定map的某个部分,而Hashtable则会锁定整个map。c++
那么既然ConcurrentHashMap那么优秀,为何还要有Hashtable的存在呢?ConcurrentHashMap能彻底替代HashTable吗?数组
HashTable虽然性能上不如ConcurrentHashMap,但并不能彻底被取代,二者的迭代器的一致性不一样的,HashTable的迭代器是强一致性的,而ConcurrentHashMap是弱一致的。 ConcurrentHashMap的get,clear,iterator 都是弱一致性的。 Doug Lea 也将这个判断留给用户本身决定是否使用ConcurrentHashMap。安全
那么什么是强一致性和弱一致性呢?数据结构
get方法是弱一致的,是什么含义?可能你指望往ConcurrentHashMap底层数据结构中加入一个元素后,立马能对get可见,但ConcurrentHashMap并不能如你所愿。换句话说,put操做将一个元素加入到底层数据结构后,get可能在某段时间内还看不到这个元素,若不考虑内存模型,单从代码逻辑上来看,倒是应该能够看获得的。多线程
下面将结合代码和java内存模型相关内容来分析下put/get方法。put方法咱们只需关注Segment#put,get方法只需关注Segment#get,在继续以前,先要说明一下Segment里有两个volatile变量:count和table;HashEntry里有一个volatile变量:value。app
Segment#put性能
V put(K key, int hash, V value, boolean onlyIfAbsent) { lock(); try { int c = count; if (c++ > threshold) // ensure capacity rehash(); HashEntry[] tab = table; int index = hash & (tab.length - 1); HashEntry first = tab[index]; HashEntry 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(key, hash, first, value); count = c; // write-volatile } return oldValue; } finally { unlock(); } }
Segment#getspa
V get(Object key, int hash) { if (count != 0) { // read-volatile HashEntry 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; }
咱们如何肯定线程1放入某个变量的值是否对线程2可见?当a hb(happen before) c时,a对c可见,那么咱们接下来咱们只要寻找put和get之间全部可能的执行轨迹上的hb关系。要找出hb关系,咱们须要先找出与hb相关的Action。为方便,这里将两段代码放到了一张图片上。.net
能够注意到,同一个Segment实例中的put操做是加了锁的,而对应的get却没有。根据hb关系中的线程间Action类别,能够从上图中找出这些Action,主要是volatile读写和加解锁,也就是图中画了横线的那些。
put操做能够分为两种状况,一是key已经存在,修改对应的value;二是key不存在,将一个新的Entry加入底层数据结构。
key已经存在的状况比较简单,即if (e != null)部分,前面已经说过HashEntry的value是个volatile变量,当线程1给value赋值后,会立马对执行get的线程2可见,而不用等到put方法结束。
key不存在的状况稍微复杂一些,新加一个Entry的逻辑在else中。那么将new HashEntry赋值给tab[index]是否能马上对执行get的线程可见呢?咱们只需分析写tab[index]与读取tab[index]之间是否有hb关系便可。
假设执行put的线程与执行get的线程的轨迹是这样的
执行put的线程 | 执行get的线程 |
⑧tab[index] = new HashEntry<K,V>(key, hash, first, value) | |
②count = c | |
③if (count != 0) | |
⑨HashEntry e = getFirst(hash); |
tab变量是一个普通的变量,虽然给它赋值的是volatile的table。另外,虽然引用类型(数组类型)的变量table是volatile的,但table中的元素不是volatile的,所以⑧只是一个普通的写操做;count变量是volatile的,所以②是一个volatile写;③很显然是一个volatile读;⑨中getFirst方法中读取了table,所以包含一个volatile读。
根据Synchronization Order,对同一个volatile变量,有volatile写 hb volatile读。在这个执行轨迹中,时间上②在③以前发生,且②是写count,③是读count,都是针对同一个volatile变量count,所以有② hb ③;又由于⑧和②是同一个线程中的,③和⑨是同一个线程中的,根据Program Order,有⑧ hb ②,③ hb ⑨。目前咱们有了三组关系了⑧ hb ②,② hb ③,③ hb ⑨,再根据hb关系是可传递的(即如有x hb y且y hb z,可得出x hb z),能够得出⑧ hb ⑨。所以,若是按照上述执行轨迹,⑧中写入的数组元素对⑨中的读取操做是可见的。
再考虑这样一个执行轨迹:
执行put的线程 | 执行get的线程 |
⑧tab[index] = new HashEntry<K,V>(key, hash, first, value) | |
③if (count != 0) | |
②count = c | |
⑨HashEntry e = getFirst(hash); |
这里只是变换了下执行顺序。每条语句的volatile读写含义同上,但它们之间的hb关系却改变了。Program Order是咱们一直拥有的,即咱们有⑧ hb ②,③ hb ⑨。但此次对volatile的count的读时间上发生在对count的写以前,咱们没法得出② hb ⑨这层关系了。所以,经过count变量,在这个轨迹上是没法得出⑧ hb ⑨的。那么,存不存在其它可替换关系,让咱们仍能得出⑧ hb ⑨呢?
咱们要找的是,在⑧以后有一条语句或指令x,在⑨以前有一条语句或指令y,存在x hb y。这样咱们能够有⑧ hb x,x hb y, y hb ⑨。就让咱们来找一下是否存在这样的x和y。图中的⑤、⑥、⑦、①存在volatile读写,可是它们在⑧以前,所以对确立⑧ hb ⑨这个关系没有用处;同理,④在⑨以后,咱们要找的是⑨以前的,所以也对这个问题无益。前面已经分析过了②,③之间无法确立hb关系。
在⑧以后,咱们发现一个unlock操做,若是能在⑨以前找到一个lock操做,那么咱们要找的x就是unlock,要找的y就是lock,由于Synchronization Order中有unlock hb lock的关系。可是,很不幸运,⑨以前没有lock操做。所以,对于这样的轨迹,是没有⑧ hb ⑨关系的,也就是说,若是某个Segment实例中的put将一个Entry加入到了table中,在未执行count赋值操做以前有另外一个线程执行了同一个Segment实例中的get,来获取这个刚加入的Entry中的value,那么是有可能取不到的!
此外,若是getFirst(hash)先执行,tab[index] = new HashEntry<K,V>(key, hash, first, value)后执行,那么,这个get操做也是看不到put的结果的。
……
正是由于get操做几乎全部时候都是一个无锁操做(get中有一个readValueUnderLock调用,不过这句执行到的概率极小),使得同一个Segment实例上的put和get能够同时进行,这就是get操做是弱一致的根本缘由。Java API中对此有一句简单的描述:
Retrievals reflect the results of the most recently completed update operations holding upon their onset.
也就是说API上保证get操做必定能看到已完成的put操做。已完成的put操做确定在get读取count以前对count作了写入操做。所以,也就是咱们第一个轨迹分析的状况。
ConcurrentHashMap#clear
clear方法很简单,看下代码即知。
public void clear() { for (int i = 0; i < segments.length; ++i) segments[i].clear(); }
由于没有全局的锁,在清除完一个segments以后,正在清理下一个segments的时候,已经清理segments可能又被加入了数据,所以clear返回的时候,ConcurrentHashMap中是可能存在数据的。所以,clear方法是弱一致的。
ConcurrentHashMap中的迭代器
ConcurrentHashMap中的迭代器主要包括entrySet、keySet、values方法。它们大同小异,这里选择entrySet解释。当咱们调用entrySet返回值的iterator方法时,返回的是EntryIterator,在EntryIterator上调用next方法时,最终实际调用到了HashIterator.advance()方法,看下这个方法:
final void advance() { if (nextEntry != null && (nextEntry = nextEntry.next) != null) return; while (nextTableIndex >= 0) { if ( (nextEntry = currentTable[nextTableIndex--]) != null) return; } while (nextSegmentIndex >= 0) { Segment<K,V> seg = segments[nextSegmentIndex--]; if (seg.count != 0) { currentTable = seg.table; for (int j = currentTable.length - 1; j >= 0; --j) { if ( (nextEntry = currentTable[j]) != null) { nextTableIndex = j - 1; return; } } } } }
这个方法在遍历底层数组。在遍历过程当中,若是已经遍历的数组上的内容变化了,迭代器不会抛出ConcurrentModificationException异常。若是未遍历的数组上的内容发生了变化,则有可能反映到迭代过程当中。这就是ConcurrentHashMap迭代器弱一致的表现。
总结
ConcurrentHashMap的弱一致性主要是为了提高效率,是一致性与效率之间的一种权衡。要成为强一致性,就获得处使用锁,甚至是全局锁,这就与Hashtable和同步的HashMap同样了。
1. http://blog.csdn.net/kobejayandy/article/details/16834311
2. http://ifeve.com/concurrenthashmap-vs-hashtable/
3. http://ifeve.com/concurrenthashmap-weakly-consistent/