Java1.7中HashMap多线程死链如何产生

67 阅读3分钟

将用“搬家大混乱”的比喻,结合JDK 1.7的HashMap源码,为你揭示多线程死循环的产生原理。这个故事会让你像看悬疑剧一样轻松理解这个经典面试问题!


🧳 ​​故事背景:搬家工人的链表反转陷阱​

想象有两个搬家工人(线程T1和T2)在搬运一条项链(链表)。项链由珠子(节点)串联而成,顺序是A→B→C。他们的任务是把项链搬到新房间(扩容),但规则很奇葩:
​必须倒序搬运(头插法)​​!新房间的项链顺序要变成C→B→A。

​正常搬家(单线程)​

  1. 工人拆下A珠 → 新房间:A
  2. 拆下B珠 → 新房间:B→A(B插在A前面)
  3. 拆下C珠 → 新房间:C→B→A

​混乱搬家(多线程)​

当两个工人同时搬运时:

image.png


⚠️ ​​一、JDK 1.7的死循环代码解析​

​关键代码:头插法扩容(Transfer方法)​

java
Copy
void transfer(Entry[] newTable) {
    for (Entry<K,V> e : oldTable) {
        while (e != null) {
            Entry<K,V> next = e.next; // ⚡️步骤1:记录下一个节点
            int newIdx = hash(e.key) & (newCapacity-1);
            e.next = newTable[newIdx]; // ⚡️步骤2:头插法(反转链表)
            newTable[newIdx] = e;      // ⚡️步骤3:新表头部指向当前节点
            e = next;                  // ⚡️步骤4:处理下一个节点
        }
    }
}

​死循环形成步骤(对照故事)​

假设初始链表:头节点 → A → B → null
​时刻0​​:

  • T1执行:e = A, next = B(记录B位置) → ​​暂停​
  • T2执行:完整搬运链表,新房间链表变为:B → A → null

​时刻1​​:

  • T1恢复执行(仍以为链表是A→B→null):

    java
    Copy
    e.next = newTable[newIdx];  // 新表当前为空,A.next = null
    newTable[newIdx] = e;       // 新表头部指向A
    e = next;                   // e = B(此时T2已把A.next改为BA
  • T1继续处理B:

    java
    Copy
    e.next = newTable[newIdx];  // B.next = A(形成BA)
    newTable[newIdx] = B;        // 新表头部指向B
    

最终链表:​​B → A → B​​(环状!)

💡 关键点:​​T1暂停时保留的next=B已失效​​(T2修改后实际B.next=A),导致头插法产生环。


🔍 ​​二、死循环的具象化演示​

​环形链表结构​

image.png

当调用map.get(key)时:

  • 程序从头部B开始遍历

  • 访问B → 访问A → 访问B → 访问A...

  • ​CPU 100%​​(无限循环无法退出)

​复现代码(JDK 1.7环境下运行)​

java
Copy
public static void main(String[] args) throws InterruptedException {
    Map<Integer, Integer> map = new HashMap<>(2, 0.75f);
    map.put(1, 1); // 初始化链表

    // 线程1:扩容时插入数据
    Thread t1 = new Thread(() -> {
        map.put(2, 2); // 触发扩容
    });

    // 线程2:同时触发扩容
    Thread t2 = new Thread(() -> {
        map.put(3, 3); // 同时触发扩容
    });

    t1.start();
    t2.start();
    t1.join();
    t2.join();

    // 此时调用get(1)可能陷入死循环!
    System.out.println(map.get(1)); 
}

🛠️ ​​三、JDK 1.8的救赎:尾插法解决死循环​

​改进代码:尾插法(避免反转)​

java
Copy
// JDK 1.8的扩容逻辑(Node替换Entry)
Node<K,V> loHead = null, loTail = null; // 低位链表头尾
Node<K,V> hiHead = null, hiTail = null; // 高位链表头尾
Node<K,V> next;
do {
    next = e.next;
    if ((e.hash & oldCap) == 0) {
        if (loTail == null) loHead = e;
        else loTail.next = e; // ⭐尾插法:新节点接在尾部
        loTail = e;
    } else {
        if (hiTail == null) hiHead = e;
        else hiTail.next = e; // ⭐尾插法
        hiTail = e;
    }
} while ((e = next) != null);

​优势​​:

  1. 保持链表原始顺序(A→B→C搬后仍是A→B→C)

  2. 多线程扩容时不会形成环(但仍有​​数据覆盖​​风险)


🛡️ ​​四、终极解决方案:告别死循环的四种武器​

​方法​​原理​​代码示例​
​1. 升级JDK 1.8+​尾插法避免死循环(但仍非线程安全)Map<K,V> map = new HashMap<>();
​2. 使用ConcurrentHashMap​分段锁/CAS保证原子操作Map<K,V> safeMap = new ConcurrentHashMap<>();26
​3. 外部加锁​手动控制并发访问synchronized(map) { <br> map.put(key, value);<br>}
​4. 只读视图​防止意外修改Map<K,V> readOnlyMap = Collections.unmodifiableMap(map);

💎 ​​工程建议​​:

  • ​高并发场景​​:必用ConcurrentHashMap(JDK 1.8采用CAS+synchronized优化)

  • ​历史系统​​:确保JDK ≥1.8并避免多线程写HashMap

  • ​面试要点​​:能画图解释JDK 1.7头插法成环过程


💎 ​​总结:一图掌握HashMap死循环本质​

image.png​面试口诀​​:

头插反转埋祸根,多线程扩容乱乾坤。
链表成环CPU爆,尾插并发定乾坤。

理解后试试在JDK 1.7环境运行复现代码(需配置旧JDK),亲眼见证死循环的产生。记住:​​多线程写HashMap永远是不定时炸弹,ConcurrentHashMap才是王道!​