ConcurrentHashMap能彻底替代HashTable吗?

关于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变量:counttable;HashEntry里有一个volatile变量:valueapp

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同样了。

Reference:

1. http://blog.csdn.net/kobejayandy/article/details/16834311

2. http://ifeve.com/concurrenthashmap-vs-hashtable/

3. http://ifeve.com/concurrenthashmap-weakly-consistent/

相关文章
相关标签/搜索