以JDK1.8的 HashMap 为例
HashMap 的 put 方法其中造成线程不安全的方法主要是 resize() 扩容方法。
1️⃣情况一:
线程 A 和线程 B 共同对 HashMap 进行 put 操作。假设 A 和 B 插入的 Key-Value 中 key 的 hashcode 是相同的,这说明该键值对将会插入到 Table 的同一个下标的位置,会发生哈希碰撞,此时 HashMap 按照平时的做法是形成一个链表(若超过八个节点则是红黑树)。现在插入的下标为 null(Table[i]==null) 则进行正常的插入,此时线程 A 进行到了这一步正准备插入,这时候线程 A 堵塞,线程 B 获得运行时间,进行同样操作,也是 Table[i]==null,此时它直接运行完整个 put 方法,成功将元素插入。随后线程 A 获得运行时间接着上面的判断继续运行,进行了 Table[i]==null 的插入(此时实际是 Table[i]!=null 的操作,因为前面线程 B 已经插入了一个元素了),这样就会直接把原来线程 B 插入的数据直接覆盖了,如此非线程安全。在 hashmap 做 put 操作的时候会调用到以下的方法:
void addEntry(int hash, K key, V value, int bucketIndex) {
Entry<K,V> e = table[bucketIndex];
table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
if (size++ >= threshold)
resize(2 * table.length);
}
A 线程和 B 线程同时对同一个数组位置调用 addEntry,两个线程会同时得到现在的头结点,然后 A 写入新的头结点之后,B 也写入新的头结点,那 B 的写入操作就会覆盖 A 的写入操作造成 A 的写入操作丢失。
2️⃣情况二:
原因是:
线程 B 获得 CPU 时 e = Entry3,next = Entry2;
正常赋值,然后进行下一次循环遍历时要注意,此时 HashMap 已经是被线程 A resize 过的了,那么就有 e = Entry2,next = Entry3;
头插法插入此时:
接着循环,e = Entry3,next = Entry3.next = null
(看图),此时再头插就会形成循环链表了。
当多个线程同时检测到总数量超过门限值的时候就会同时调用 resize 操作,各自生成新的数组并 rehash 后赋给该 map 底层的数组 table,结果最终只有最后一个线程生成的新数组被赋给table变量,其他线程的均会丢失。而且当某些线程已经完成赋值而其他线程刚开始的时候,就会用已经被赋值的 table 作为原始数组,这样也会有问题。
网友评论