大家在面试的时候可能都听过“JDK8以前用头插法会死锁”这句话,但这到底是怎么回事呢?为啥JDK8换成尾插法就没事了?咱们直接看看JDK7的put方法就知道了。先来看这个方法的逻辑:首先判断TABLE是不是EMPTY_TABLE,如果是的话,就调用inflateTable把它扩容到16;如果key是null,就直接插到table[0]的链表头部,不用管扩容的事儿;接下来计算hash值,看看这个位置有没有节点,如果有的话就调用recordAccess并返回旧值;最后如果位置是空的,就调用addEntry开始真正的数据迁移。这addEntry干的其实就是两件事:先把老表的长度翻倍,然后调用transfer把数据转移到新表里去。transfer真正干活的时候,JDK7就是用了头插法,它把链表的元素反着插进去。比如说原数组下标0的地方挂着A→B→C,扩容后新数组的0下标也需要挂链表,头插法就会先让C进去,再是B,最后是A,顺序完全反过来了。 这时候问题就来了,要是有两个线程A和B同时去操作同一个桶扩容怎么办?A先拿到锁,把A→B→C的节点反着插进了新表里;B后到,发现老表空了,就把C→B→A也反着插进了新表的头部。这时候A想把自己插进去发现位置被占了,B也想插进去发现位置也被占了,双方都在等对方释放锁,死锁就形成了。所以JDK7用头插法和链表反转把扩容时机跟节点插入顺序绑在了一起,多线程操作的时候很容易造成循环等待。JDK8就不一样了,它用了尾插法还加了红黑树优化,插入顺序不再依赖桶锁的顺序,死锁概率就大幅下降了。如果想彻底搞懂非线程安全的JDK8版HashMap或者线程安全的ConcurrentHashMap,可以顺着文章往下看。