JDK 7 HashMap 并发死链
死链代码演示
测试代码
注意
要在 JDK 7 下运行,否则扩容机制和 hash 的计算方法都变了
死链复现
调试工具使用 idea
在 HashMap 源码 590 行加断点
int newCapacity = newTable.length
断点的条件如下,目的是让 HashMap 在扩容为 32 时,并且线程为 Thread-0 或 Thread-1 时停下来
断点暂停方式选择 Thread,否则在调试 Thread-0 时,Thread-1 无法恢复运行。
运行代码,程序在预料的断点位置停了下来,输出
接下来进入扩容流程调试,在 HashMap 源码 594 行加断点
这是为了观察 e 节点和 next 节点的状态,Thread-0 单步执行到 594 行,再 594 处再添加一个断点(条件Thread.currentThread().getName().equals("Thread-0"))这时可以在 Variables 面板观察到 e 和 next 变量,使用 view as -> Object 查看节点状态
在 Threads 面板选中 Thread-1 恢复运行,可以看到控制台输出新的内容如下,Thread-1 扩容已完成 扩容后大小:13
这时 Thread-0 还停在 594 处, Variables 面板变量的状态已经变化为
为什么呢,因为 Thread-1 扩容时链表也是后加入的元素放入链表头,因此链表就倒过来了,但 Thread-1 虽然结果正确,但它结束后 Thread-0 还要继续运行
接下来就可以单步调试(F8)观察死链的产生了
下一轮循环到 594,将 e 搬迁到 newTable 链表头
下一轮循环到 594,将 e 搬迁到 newTable 链表头
再看看源码
源码分析
假设 map 中初始元素是
究其原因,是因为在多线程环境下使用了非线程安全的 map 集合
JDK 8 虽然将扩容算法做了调整,不再将元素加入链表头(而是保持与扩容前一样的顺序),但仍不意味着能够在多线程环境下能够安全扩容,还会出现其它问题(如扩容丢数据)