目录
1.并发add相同hash值or相同key的元素,导致丢失
Jdk1.7源码
|
void addEntry(int hash, K key, V value, int bucketIndex) { if ((size >= threshold) && (null != table[bucketIndex])) {//扩容 resize(2 * table.length); hash = (null != key) ? hash(key) : 0; bucketIndex = indexFor(hash, table.length); } //将新增的元素放到i位置,并将它的next指向旧的元素 Entry<K,V> e = table[bucketIndex];//当前i位置的元素 table[bucketIndex] = new Entry<>(hash, key, value, e); size++; } |
当A、B两个线程并发add相同hash值元素时候,当A、B都执行完毕黄色部分后,A、B获取的e都为otherValue。
假设A快一步执行绿色部分,此时内存中存储如下:
|
|
下一步,B执行绿色部分,此时内存中存储如下:
|
|
然后就是B线程的值覆盖了A线程的,导致A线程add的值丢失。
原文:https://blog.csdn.net/lan861698789/article/details/81697398
2. Rehash导致死循环
在扩容时会做一个rehash的操作,这里会导致死循环。
Jdk1.7 Rehash代码:
|
//对老数据进行重新计算hash值,重新做数据下标映射, //然后全部复制到新数组 void resize(int newCapacity) { Entry[] oldTable = table; int oldCapacity = oldTable.length;
Entry[] newTable = new Entry[newCapacity]; transfer(newTable, initHashSeedAsNeeded(newCapacity)); table = newTable; threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1); } 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; 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; } } } |
resize步骤如下:
1.对数组table中的元素遍历
2.对链表上的每一个节点遍历:用 next 取得要转移那个元素的下一个,将 e 转移到新 Hash 表的头部,使用头插法插入节点。
3.循环2,直到链表节点全部转移
4.循环1,直到所有索引数组全部转移
经过这几步,我们会发现转移的时候是逆序的。假如转移前链表顺序是1->2->3,那么转移后就会变成3->2->1。这时候就有点头绪了,死锁问题不就是因为1->2的同时2->1造成的吗?所以,HashMap 的死锁问题就出在这个transfer()函数上。
先演示单线程
单线程情况下,rehash 不会出现任何问题:
假设hash算法就是最简单的 key mod table.length(也就是数组的长度)。
最上面的是old hash 表,其中的Hash表的 size = 2, 所以 key = 3, 7, 5,在 mod 2以后碰撞发生在 table[1]
接下来的三个步骤是 Hash表 resize 到4,并将所有的 <key,value> 重新rehash到新 Hash 表的过程
如图所示:
多线程时候
为了思路更清晰,我们只将关键代码展示出来
|
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; 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、B两个线程都执行resize(),然后都执行完了红色部分,这时候两个的e都是key3,next都是key7.
然后A让出cpu,B全部执行完成,此时内存数据结构如下:
此时线程A的key还是key3,next还是key7。(而key7的next却是key3了)
线程A继续执行,插入table[i],next指向以前的table[i],也就是key7
内存结构图如下:
通过图我们发现,此时进入死循环了。