12-集合-1-HashMap

插入(源码)

// 保存一个KEY-VALUE键值对,如果KEY值已经存在,覆盖旧VALUE
public V put(K key, V value) {
	return putVal(hash(key), key, value, false, true);
}

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
			  boolean evict) {
	Node<K,V>[] tab; Node<K,V> p; int n, i;
	// 如果HashTable的内部数组没有初始化,完成初始化(HashTable的内部数组(Node<K,V>[] table)在第一次使用时初始化,在扩容时修改)
	if ((tab = table) == null || (n = tab.length) == 0)
		// resize()函数完成对内部数组table的初始化或双倍扩容(新容量是旧容量的两倍)
		n = (tab = resize()).length;
	// 如果hash位置没有对应头节点
	if ((p = tab[i = (n - 1) & hash]) == null)
		// 直接将新节点存储到内部数组中
		tab[i] = newNode(hash, key, value, null);
	// 如果hash位置已经有了数据(发生哈希碰撞)
	else {
		Node<K,V> e; K k;
		// 如果KEY值已经保存了,记录;
		if (p.hash == hash &&
			((k = p.key) == key || (key != null && key.equals(k))))
			e = p;
		// 如果已经使用红黑树保存数据节点,将节点加入或更新到树;
		else if (p instanceof TreeNode)
			e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
		// 如果还是链表
		else {
			// 从首节点开始遍历链表节点
			for (int binCount = 0; ; ++binCount) {
				// 如果遍历到尾节点,将数据加入到链表末尾
				if ((e = p.next) == null) {
					p.next = newNode(hash, key, value, null);
					// 如何链表长度大于等于7,尝试转换成红黑树
					if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
						// 如果HashTable内部数组长度小于64,对内部数组进行扩容操作,如果长度不小于64,将当前链表转换为红黑树
						treeifyBin(tab, hash);
					break;
				}
				//如果在链表中找到KEY,结束遍历
				if (e.hash == hash &&
					((k = e.key) == key || (key != null && key.equals(k))))
					break;
				
				p = e;
			}
		}
		// 如果找到了KEY
		if (e != null) { // existing mapping for key
			V oldValue = e.value;
			// 如果允许替换值(HashTable默认允许),使用新VALUE替换旧VALUE
			if (!onlyIfAbsent || oldValue == null)
				e.value = value;
			// LinkedHashMap中的一些功能,HashTable中没有意义
			afterNodeAccess(e);
			// 返回旧VALUE
			return oldValue;
		}
	}
	// 如果是新增的值(如果找到了KEY,会在此代码前返回,此处不会执行),增加HashTable被修改次数
	// modCount用于记录HashTable中节点数被修改或HashTable结构被修改(像扩容),在Iterator遍历节点时,防止遍历时非正常修改数据;
	++modCount;
	// 增加节点总数,如果节点总数大于扩容阈值(HashTable内部数组容量*0.75),进行扩容
	if (++size > threshold)
		resize();
	// 此操作在HashTable中无意义
	afterNodeInsertion(evict);
	return null;
}

扩容

HashTable扩容过程中会有拆链和拆树的动作,具体说明参考下面的代码注释

final Node<K,V>[] resize() {
	Node<K,V>[] oldTab = table;
	// 获取数组旧的的长度,如果为数组未初始化,则设置旧数组长度为0;
	int oldCap = (oldTab == null) ? 0 : oldTab.length;
	// 获取数组需要扩容的临界点(数组长度*0.75)
	int oldThr = threshold;
	int newCap, newThr = 0;
	// 如果数组已经初始化
	if (oldCap > 0) {
		// 如果数组长度超过了最大容量
		if (oldCap >= MAXIMUM_CAPACITY) {
			// 将数组下次扩容临界值设置为整形最大值(不会再触发扩容)
			threshold = Integer.MAX_VALUE;
			return oldTab;
		}
		// 如果本次扩容后的长度(当前数组长度的二倍)不超过了最大值,并且数组已经初始化(当前数组长度不小于初始值16)
		else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
				 oldCap >= DEFAULT_INITIAL_CAPACITY)
			 // 将新的扩容临界值设置为原来的二倍
			newThr = oldThr << 1; // double threshold
	}
	// 如果数组未初始化并指定了数组初始长度(在HashTable创建时,可以指定数组长度),使用初始扩容临界值作为新数组长度。(初始临界值为指定的数组长度)
	else if (oldThr > 0) // initial capacity was placed in threshold
		newCap = oldThr;
	// 如果数组未初始化并且未指定初始容量,使用默认初始化值
	else {        // zero initial threshold signifies using defaults
		newCap = DEFAULT_INITIAL_CAPACITY;
		newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
	}
	// 如果数组未初始化并指定了数组初始长度,
	if (newThr == 0) {
		// 计算扩容临界值
		float ft = (float)newCap * loadFactor;
		// 如果新数组长度超过最大值或者新扩容临界值超过最大值,新扩容临界值为整形最大值(之后不会再触发扩容)
		newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
				 (int)ft : Integer.MAX_VALUE);
	}
	threshold = newThr;
	@SuppressWarnings({"rawtypes","unchecked"})
	// 新建数组替换旧的数组
	Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
	table = newTab;
	// 如果数组已经初始化,将现有数据转移到新数组中
	if (oldTab != null) {
		// 遍历数组
		for (int j = 0; j < oldCap; ++j) {
			Node<K,V> e;
			// 如果数组当前位数据不为空,则转移到新数组中
			if ((e = oldTab[j]) != null) {
				oldTab[j] = null;
				// 如果当前位只有一个数据
				if (e.next == null)
					// 重新计算数据在新数组中的位置并移动
					newTab[e.hash & (newCap - 1)] = e;
				// 如果当前位的数据是红黑树,转移到新数组中(如果树节点个数小于6,将树变成链表,否则,转移成新树)
				// 在新数组中,树有可能被拆分成两颗树,放大当前下标和当前下标+oldCap的下标上,具体原因参考下面链表的处理说明;
				else if (e instanceof TreeNode)
					((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
				// 如果当前位是链表
				else { // preserve order
					Node<K,V> loHead = null, loTail = null;
					Node<K,V> hiHead = null, hiTail = null;
					Node<K,V> next;
					
					// 逐个处理链表节点
					// 遍历链表的过程中,会将当前链表转换为两个新链表,两个新链表在新数组中的下标分别是当前下标和当前下标加原有数组长度(假设原数组长度为16,扩容后为32,当前下标为2,则两个新链表在新数组的下标分别为2和18)
					// HashTable的数组长度为2的整数倍,计算下标的公式为 【下标=(key的hash & (数组长度 - 1))】,假设原数组长度为16(10000),扩容后长度为32(100000);数据key的hash为18(10010),
					// 那么原下标的计算公式为 【10进制(15 & 18) = 2进制(1111 & 10010) = 2进制(10) = 10进制(2)】,
					// 新下标的计算公式为 【10禁止(31 & 18) = 2进制(11111 & 10010) = 2进制(10010) = 10进制(18)】,而如果Key的hash为2(10)或34(100010),不是18,那么新旧下标的都为2,不会改变;
					// 通过上面的计算可以看出来,由于数组长度是以2的倍数增加的,新下标的计算多了一个高位值(有之前的15四个1,变成了31五个1),新下标要么与原来相同(Hash的新增的一个高位对应的二进制位0),
					// 要么是原来下标值加上原数组长度(Hash的新增的一个高位对应的二进制位1)。
					do {
						next = e.next;
						// 如果当前节点在新数组的下标与原来下标相同
						// oldCap是2的整数倍,那么二进制表示时,只有一位为1,这一位是新下标计算是新增的位,如果Key的Hash值二进制表示时此位的值为0,则新下标与原下标相同,否则,新下标是原下标加上oldCap。
						if ((e.hash & oldCap) == 0) {
							if (loTail == null)
								loHead = e;
							else
								loTail.next = e;
							loTail = e;
						}
						// 如果新下标是原下标加上oldCap;
						else {
							if (hiTail == null)
								hiHead = e;
							else
								hiTail.next = e;
							hiTail = e;
						}
					} while ((e = next) != null);
					// 将处理后的两条新链放到对应位置
					if (loTail != null) {
						loTail.next = null;
						newTab[j] = loHead;
					}
					if (hiTail != null) {
						hiTail.next = null;
						newTab[j + oldCap] = hiHead;
					}
				}
			}
		}
	}
	return newTab;
}

问题

CPU100%

节点成为环路的原因是头插法造成的(Jdk1.8之前,HashTable和HashMap都是头插法,1.8后,hashMap该为尾插法);根据经验,新插入的数据被查询的概率比较高,头插法可以提高平均查询速度,但是在扩容的过程中,头插发会颠倒节点链的顺序,可以产生节点环路;

解决办法
改为尾插法,这样就防止的环路的产生;

数据丢失问题

问题描述
多线程中,当扩容或新数据插入时,后执行线程会覆盖到前一个线程的修改结果,造成数据丢失

解决办法
(1)CurrentHashtabel原理(后续补充)
(2)自己设计一个
在hashTable保留一个扩容代的变量(int volatile generation),每个节点中也创建一个同样的变量;
在扩容时,首先检查节点的扩容代与hashTable的是否一致,如果一直,不进行扩容处理,进行下一个,否则进行扩容处理,CAS修改节点扩容代

posted @ 2020-10-22 09:40  donfaquir  阅读(91)  评论(0编辑  收藏  举报