HashMap
1.8之前扩容方法resize()采用头插方法
如果数组元素为链表时,扩容resize方法是先将链表尾部重新rehash,插入新数组,然后插入尾部的parent,依次类推
该方法是在一个while()循环中的,仅当e == null时才能退出循环:
void transfer(Entry[] newTable, boolean rehash) {
int newCapacity = newTable.length;
for (Entry<K,V> e : table) {
while(null != e) {
Entry<K,V> next = e.next;//——————(1)
if (rehash) {
e.hash = null == e.key ? 0 : hash(e.key);
}
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
e = next;
}
}
}
但在多线程情况下,可能存在线程A运行到(1)处时,失去时间片;线程B将该链表的扩容执行完毕。
此时由于resize()采用的是头插,如果链表中前后元素仍然插入到同一个位置,链表应有的顺序反过来了;
这时A恢复了执行,走的仍然是B线程该走的路;但此时由于顺序反过来了,但A线程并不知道,照原有的逻辑依次走下去,原来链表尾端的next将会是null的,但线程B将链表反转了,此时链表尾部变成了链表的头部,next不再是null而可能会变成原来链表中的某个元素,这样的链表,就不再会出现e==null的元素了。也就是网上说的,链表成环。
我个人觉得成环虽然是一种形象的说法,但我觉得我更能接受的说法是:由于java并没有对HashMap做并发控制(没错,不管1.8以前还是1.8以后,1.8以后只是改变了插入流程:
next = e.next;
//比较容易造成迷惑的是这里。首先不论从扩容还是从初始化上看oldCap都是2的整数次方,
//所以这里的 e.hash & oldCap
//(有点不理解是不是,你想啊:一般取余是e.hash & (oldCap - 1),这里 & 个 oldCap是什么意思?)
//那我们往高一位看,oldCap的二进制数只有一位为1(看注释第一句),
//而这为1的那位,刚好是oldCap-1(底下全为1用来判断余数的),
//所以这一位是干嘛的呢,是判断e.hash是否为 oldCap的奇数倍的。e.hash在这位为1时,才是oldCap的奇数倍
//这样在扩容两倍的时候,就会转一个圈回来,j仍然是j。
//如果是偶数倍,则不用说了,转一圈还要多出一个oldCap的偏移量
//这也就是下面数组放的位置的缘由
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
//尾插本插,这就保证了不管咋并发,就往尾部塞就是了,注意啊,它插得可不是原来的e,它插的是tail
//所以和while的终止条件while ((e = next) != null);不会产生冲突
//而且这个是局部变量,所以怎么插都不会影响别个线程。
loTail.next = e;
loTail = e;
}
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
//loTail和hiTail这两个是根据rehash结果不同,分别存在两个不同的数组中一个下标是原来的j另外一个是j+oldCap偏移量,由于判断过原体量并不会触发treeify,所以分割后很大可能链表长度也变小了,更不会触发。
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
)
中间插得有点多,那我们话又说回来:由于java并没有对HashMap做并发控制,且在原有的链表元素上进行操作,导致了循环终止条件永远无法满足。造成了无法return的死循环,让CPU的load增高。
这里我只读了1.8的源码
很好,解决了HashMap链表扩容死循环问题。
那么就安全了么?
有没有听说过红黑树:java引进了红黑树,解决了Hash分布极端不均匀情况下,元素全部插到一条链表里,使HashMap退化成了一条链表,降低HashMap的查找效率,这些…都背…咳咳,都知道吧?
那么,我们来看下TreeNode吧
*/
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
TreeNode<K,V> parent; // red-black tree links
TreeNode<K,V> left;
TreeNode<K,V> right;
TreeNode<K,V> prev; // needed to unlink next upon deletion
boolean red;
TreeNode(int hash, K key, V val, Node<K,V> next) {
super(hash, key, val, next);
}
//..........省略
}
好,看到这些属性没。
再看看介两过:
/**
* Returns root of tree containing this node.
*/
final TreeNode<K,V> root() {
for (TreeNode<K,V> r = this, p;;) {
if ((p = r.parent) == null)
return r;
r = p;
}
}
static <K,V> TreeNode<K,V> balanceInsertion(TreeNode<K,V> root,
TreeNode<K,V> x) {
x.red = true;
for (TreeNode<K,V> xp, xpp, xppl, xppr;;) {
if ((xp = x.parent) == null) {
x.red = false;
return x;
}
//....省略
}
以及万恶的源泉:
/**
* Tree version of putVal.
*/
final TreeNode<K,V> putTreeVal(HashMap<K,V> map, Node<K,V>[] tab,
int h, K k, V v) {
Class<?> kc = null;
boolean searched = false;
TreeNode<K,V> root = (parent != null) ? root()/**看这里**/ : this;
for (TreeNode<K,V> p = root;;) {
int dir, ph; K pk;
if ((ph = p.hash) > h)
dir = -1;
else if (ph < h)
dir = 1;
else if ((pk = p.key) == k || (k != null && k.equals(pk)))
return p;
else if ((kc == null &&
(kc = comparableClassFor(k)) == null) ||
(dir = compareComparables(kc, k, pk)) == 0) {
if (!searched) {
TreeNode<K,V> q, ch;
searched = true;
if (((ch = p.left) != null &&
(q = ch.find(h, k, kc)) != null) ||
((ch = p.right) != null &&
(q = ch.find(h, k, kc)) != null))
return q;
}
dir = tieBreakOrder(k, pk);
}
TreeNode<K,V> xp = p;
if ((p = (dir <= 0) ? p.left : p.right) == null) {
Node<K,V> xpn = xp.next;
TreeNode<K,V> x = map.newTreeNode(h, k, v, xpn);
if (dir <= 0)
xp.left = x;
else
xp.right = x;
xp.next = x;
x.parent = x.prev = xp;
if (xpn != null)
((TreeNode<K,V>)xpn).prev = x;
moveRootToFront(tab, balanceInsertion(root, x)/**再看这里**/);
return null;
}
}
}
当然不止这些,查root()就可以看到:
很好,有put那自然就要有remove,不然像什么话嘛。
我们看看:
static <K,V> TreeNode<K,V> balanceDeletion(TreeNode<K,V> root,
TreeNode<K,V> x) {
for (TreeNode<K,V> xp, xpl, xpr;;) {
if (x == null || x == root)
return root;
else if ((xp = x.parent) == null) {
x.red = false;
return x;
}
对,不用看了,这个就是那个什么remove中调用的方法。
太 危 险 啦 !
虽然不知道并发情况下是什么样的,但我知道TreeNode里面那些元素都是可以在并发情况下发生改变的。
像什么:r.parent啊,x.parent啊,之类的,在判断的时候都是可以变的。
我看了下网上(复z制去到ρĨη⫐Ǖ℺ⓓǘὂ(划掉 google:HashMap1.8死循环)死循环拉出来 架烧烤架上的 基本是root()这个方法:
/**
* Returns root of tree containing this node.
*/
final TreeNode<K,V> root() {
for (TreeNode<K,V> r = this, p;;) {
if ((p = r.parent) == null)
return r;
r = p;
}
}
理解一哈,毕竟人家只有一个return,容错概率小。
像什么balanceDeletion()啊、balanceInsertion()啊;人家的return就很多。容错概率大一些。
所以一般都比较隐蔽,我看了几篇文章(我写这篇也是由于突然看到1.8以后的HashMap也会产生load100%的问题,有点颠覆常识,就决定查查资料看看原因,但好像没几篇点的让我感觉很明晰),好像只有一个兄弟点出了balanceInsertion()也会造成死循环,并指明了大致原因。(为我指明了方向,在那些下面,大部分朋友的反馈,基本就是哪怕是root()方法的死循环,复现都比较困难)。