话说在JDK7的世界里,“用头插法会死锁”这句话可是面试的常考题。那为什么改成JDK8以后用尾插法就没事了呢?咱们不如直接钻到JDK7的源码里,看看里面到底发生了什么。 JDK7的put方法其实也不复杂,主要分四步走。第一步得看数组 TABLE 是不是 EMPTY_TABLE,如果是的话,就赶紧调用 inflateTable 把它扩容到默认的16个槽位。如果 key 是 null,那也简单,直接插到 table[0] 的链表头上就行。 接下来的重头戏就是计算哈希值了。通过 hash(key) 异或上 indexFor(hash, table.length),咱们就能找到要放的位置。要是这里已经有别的节点了,就调用 e.recordAccess(this) 返回旧值;如果是空的,那就要调用 addEntry 来正式进入扩容和迁移的流程了。 addEntry 里面也就干了两件事:先把原来的数组长度翻倍(resize),再调用 transfer 把老数据搬过去。关键就在这里,JDK7 的 transfer 是用的头插法——把链表上的元素反着顺序插入到新表中。 举个例子吧,假设原来下标0的地方挂着 A→B→C 这一串节点,扩容后新表的下标0处也得挂一串。头插法会让 C 先进去,接着是 B,最后才是 A,顺序完全反过来了。 这下问题就来了。假设有两个线程 A 和 B 同时来扩容同一个桶。A 先拿到锁,把 A→B→C 这几个节点反着往新表里插;B 后到了,一看老表已经被清空了,就开始在新表的头部插入 C→B→A。这时候 A 正想把 A 插进去,发现 B 正在把 C 插进去;而 B 也在等 A 把 B 插好。这一来一回,双方都在等对方释放锁,结果就是彻底死锁了。 为啥偏偏是 JDK7 出这问题呢?因为它把扩容时机和节点插入顺序死死绑在一起了。多线程一搞操作,特别容易形成循环等待。反观 JDK8 就不一样了,它用的是尾插法,还引入了红黑树来优化。节点的顺序不再跟桶锁的顺序挂钩了,死锁的概率自然就大大下降了。 想彻底搞懂非线程安全的 JDK8 HashMap 或者线程安全的 ConcurrentHashMap,咱们可以接着往下看。