面试官问HashMap为啥线程不安全,我把源码翻给他看
2026/9/11 22:29:26 网站建设 项目流程

“HashMap是线程不安全的,并发环境下用ConcurrentHashMap。”这句话几乎每个Java程序员都背得滚瓜烂熟。

但面试官如果追问一句:“具体哪里不安全?能说说是哪个操作、什么条件下触发的不安全吗?”很多人的答案就开始变得模糊了——“呃……好像是put的时候会死循环?”“不对吧,好像JDK 8修复了死循环?”“那就是数据会丢?”

说实话,背八股文谁都会。但如果你真的打开HashMap的源码,一行一行地看,你会发现那些“线程不安全”的结论,其实就明明白白地写在代码里。

先说结论:死循环在JDK 8已经修复了

“HashMap在并发put时会死循环”——这个说法在JDK 7时代确实成立。原因是JDK 7在扩容时采用头插法,将旧链表的元素逐个转移到新数组时,链表顺序会被反转。多线程并发扩容时,两个线程同时操作同一个链表,就可能形成环形链表。当get()遍历到环上时,永远走不到头,CPU直接飙到100%。

JDK 8改用了尾插法,在扩容转移时保持链表原有顺序不变,环形链表的场景被彻底避免了。所以如果再拿“死循环”说事,说明你的知识点还停留在JDK 7时代。

真正核心的问题:数据覆盖

死循环修复了,但HashMap在并发下仍然是“不安全的”。打开JDK 8的源码,看putVal()方法的核心片段:

java
复制
下载
if ((p = tab[i = (n 1) & hash]) == null) tab[i] = newNode(hash, key, value, null);

这行代码的意思是:如果数组当前位置为空,就把新节点放进去。看起来没问题对吧?但问题在于,这不是一个原子操作

线程A和线程B同时执行到这一步,都判断出tab[i] == null,然后各自创建新节点并写入数组。后写入的会直接覆盖先写入的。线程A put进去的key-value对就这样凭空消失了。

这不是理论推演,而是真实会发生的情况。在压力测试中,两个线程同时put不同的key到同一个空槽位,丢失数据的概率相当高。

数据覆盖不止这一处

再看putVal()中处理哈希冲突的逻辑:

java
复制
下载
if (e != null) { // existing mapping for key V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); return oldValue; }

这里同样存在覆盖问题。两个线程同时put同一个key,都获取到了同一个e节点,然后各自更新e.value。后更新的覆盖先更新的,没有任何同步保护。前一个写入的值被悄无声息地覆盖了。

size也永远算不准

HashMapsize字段记录键值对数量,每次putremove都会执行++size--size。同样,这不是原子操作。

两个线程同时put,同时执行++size,如果初始size=0,两个线程各自读到0,计算后各自写回1——丢了两次插入,size却是1。更离谱的是,如果用size()方法拿到的值偏小,还可能导致扩容时机的误判——本来应该扩容了,因为size偏小没触发,导致链表越来越长,查询性能急剧下降。

扩容时同样不安全

JDK 8的扩容方法resize()里,有一个关键的transfer过程,将老数组的元素迁移到新数组。这个过程涉及对Node.next指针的读写。如果两个线程同时触发扩容,A线程修改了链表指针指向新数组,B线程还拿着旧数组的引用继续操作,节点的next指针就可能被写乱——倒不一定是死循环,但遍历时丢失节点、get不到值是家常便饭。

那ConcurrentHashMap是怎么解决的?

对比着看就更清楚了:

数据覆盖问题:ConcurrentHashMap用CAS+synchronized,在tabAtcasTabAt中保证写入的原子性

size不准问题:用LongAdder思想维护baseCount+CounterCell数组,并发累加时分散到多个计数器上

扩容问题:多线程协同扩容,每个线程负责一段区间,用sizeCtl状态变量做全局协调

总结

回到面试场景。当面试官问“HashMap为什么线程不安全”,与其背一句“因为并发put会导致死循环和数据丢失”,不如这样回答:

“HashMap线程不安全主要集中在三个层面。第一,put操作中的数组槽位写入和链表节点更新不是原子的,会导致数据覆盖;第二,size字段的累加不是原子的,会导致集合大小不准确,进而影响扩容判断;第三,扩容过程中多个线程同时操作节点引用,可能导致节点丢失或链表结构异常。其中JDK 7的头插法环形链表问题在JDK 8已经通过尾插法修复,但数据覆盖和size不准的问题依然存在。在并发场景下,应该使用ConcurrentHashMap。”

然后你可以掏出手机,打开JDK源码,把putVal()里那几行关键代码指给他看。

背结论能过面试,读源码才能让人信服。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询