ConcurrentHashMap中rehash函数理解

Posted 秋风兮月

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了ConcurrentHashMap中rehash函数理解相关的知识,希望对你有一定的参考价值。

最近看了ConcurrentHashMap的源码,对于这个类的整体原理的讲解,请参考

探索 ConcurrentHashMap 高并发性的实现机制     这篇文章将ConcurrentHashMap的工作机制已经讲得很清楚了,结合源代码和相关注释,就可以很好地理解这个类的工作原理了。

这里补充一下ConcurrentHashMap中rehash函数的运行原理,因为这个地方我看了好长时间才理解是怎么回事。其实这个函数的注释也是解释的比较清楚,但是有些地方只有真正理解了,才能更好地理解注释说的是什么。

下面先把这个函数的代码和一些我自己的注释贴出来

void rehash() 
	HashEntry<K,V>[] oldTable = table;
	int oldCapacity = oldTable.length;
	if (oldCapacity >= MAXIMUM_CAPACITY)
		return;

	/*
	 * Reclassify nodes in each list to new Map.  Because we are
	 * using power-of-two expansion, the elements from each bin
	 * must either stay at same index, or move with a power of two
	 * offset. We eliminate unnecessary node creation by catching
	 * cases where old nodes can be reused because their next
	 * fields won't change. Statistically, at the default
	 * threshold, only about one-sixth of them need cloning when
	 * a table doubles. The nodes they replace will be garbage
	 * collectable as soon as they are no longer referenced by any
	 * reader thread that may be in the midst of traversing table
	 * right now.
	 */
	 /*
	 * 其实这个注释已经解释的很清楚了,主要就是因为扩展是按照2的幂次方
	 * 进行扩展的,所以扩展前在同一个桶中的元素,现在要么还是在原来的
	 * 序号的桶里,或者就是原来的序号再加上一个2的幂次方,就这两种选择。
	 * 所以原桶里的元素只有一部分需要移动,其余的都不要移动。该函数为了
	 * 提高效率,就是找到最后一个不在原桶序号的元素,那么连接到该元素后面
	 * 的子链表中的元素的序号都是与找到的这个不在原序号的元素的序号是一样的
	 * 那么就只需要把最后一个不在原序号的元素移到新桶里,那么后面跟的一串
	 * 子元素自然也就连接上了,而且序号还是相同的。在找到的最后一个不在
	 * 原桶序号的元素之前的元素就需要逐个的去遍历,加到和原桶序号相同的新桶上
	 * 或者加到偏移2的幂次方的序号的新桶上。这个都是新创建的元素,因为
	 * 只能在表头插入元素。这个原因可以参考
	 * 《探索 ConcurrentHashMap 高并发性的实现机制》中的讲解
	 */

	HashEntry<K,V>[] newTable = HashEntry.newArray(oldCapacity<<1);
	threshold = (int)(newTable.length * loadFactor);
	int sizeMask = newTable.length - 1;
	for (int i = 0; i < oldCapacity ; i++) 
		// We need to guarantee that any existing reads of old Map can
		//  proceed. So we cannot yet null out each bin.
		HashEntry<K,V> e = oldTable[i];

		if (e != null) 
			HashEntry<K,V> next = e.next;
			int idx = e.hash & sizeMask;

			//  Single node on list
			if (next == null)
				newTable[idx] = e;

			else 
				// Reuse trailing consecutive sequence at same slot
				HashEntry<K,V> lastRun = e;
				int lastIdx = idx;
				for (HashEntry<K,V> last = next;
					 last != null;
					 last = last.next) 
					int k = last.hash & sizeMask;
					// 这里就是遍历找到最后一个不在原桶序号处的元素
					if (k != lastIdx) 
						lastIdx = k;
						lastRun = last;
					
				
				// 把最后一个不在原桶序号处的元素赋值到新桶中
				// 由于链表本身的特性,那么该元素后面的元素也都能连接过来
				// 并且能保证后面的这些元素在新桶中的序号都是和该元素是相等的
				// 因为上面的遍历就是确保了该元素后面的元素的序号都是和这个元素
				// 的序号是相等的。不然遍历中还会重新赋值lastIdx
				newTable[lastIdx] = lastRun;

				// Clone all remaining nodes
				// 这个就是把上面找到的最后一个不在原桶序号处的元素之前的元素赋值到
				// 新桶上,注意都是把元素添加到新桶的表头处
				for (HashEntry<K,V> p = e; p != lastRun; p = p.next) 
					int k = p.hash & sizeMask;
					HashEntry<K,V> n = newTable[k];
					newTable[k] = new HashEntry<K,V>(p.key, p.hash,
													 n, p.value);
				
			
		
	
	table = newTable;

这个函数里之前最让我迷惑的就是那段遍历找最后一个不在原桶序号处元素的代码

for (HashEntry<K,V> last = next;
	 last != null;
	 last = last.next) 
	int k = last.hash & sizeMask;
	if (k != lastIdx) 
		lastIdx = k;
		lastRun = last;
	

newTable[lastIdx] = lastRun;

当时我主要想不明白的就是newTable[lastIdx] = lastRun;这里。我刚开始会觉着那么新桶里如果这个lastIdx位置已经有元素了,怎么办,岂不是就给覆盖掉了。

后来发现这种情况是不可能出现的。这还是因为table的大小是按照2的幂次方的方式去扩展的。

假设原来table的大小是2^k大小,那么现在新table的大小是2^(k+1)大小。而获取序号的方式是

int idx = e.hash & sizeMask;

而sizeMask = newTable.length - 1  即sizeMask = 11...1,即全是1,共k个1。获取序号的算法是用元素的hash值与sizeMask做与的操作。这样得到的idx实际上就是元素的hashcode值的低k位的值。而原table的sizeMask也全是1的二进制,不过总共是k-1位。那么原table的idx就是元素的hashcode的低k-1位的值。所以说如果元素的hashcode的第k为如果是0,那么元素在新桶的序号就是和原桶的序号是相等的。如果第k位的值是1,那么元素在新桶的序号就是原桶的序号+(2^k-1)。所以说只可能是这两个值。那么上面的那个newTable[lastIdx] = lastRun;就没问题了,newTable中新序号处此时肯定是空的。




以上是关于ConcurrentHashMap中rehash函数理解的主要内容,如果未能解决你的问题,请参考以下文章

多线程(十六ConcurrentHashMap原理扩容)

简单总结ConcurrentHashMap

JUC之ConcurrentHashMap源码之扩容

ConcurrentHashMap怎么保证安全的

并发容器和框架之ConcurrentHashMap

深入并发包 ConcurrentHashMap 源码解析