HashMap之环形链表源码图解

HashMap之环形链表源码图解

Java7在多线程操作HashMap时,并发扩容可能会导致环形链表,get方法时则可能出现死循环。以下是环形链表的具体产生过程。

1.扩容逻辑实现transfer()方法:

void transfer(Entry[] newTable, boolean rehash) {
    int newCapacity = newTable.length;
    for (Entry<K,V> e : table) {
          // 循环数组中元素,依次从copy到newTable
        while(null != e) {
            Entry<K,V> next = e.next;
            if (rehash) {
                e.hash = null == e.key ? 0 : hash(e.key);
            }
            int i = indexFor(e.hash, newCapacity);
               // 注意这句代码很重要,头插发扩容,会将元素e扩容到newTable的头位置,已有的newTable[i]会放到e的next。
               e.next = newTable[i];
            newTable[i] = e;
            e = next;
        }
    }
}

2.扩容前的HashMap数据:
在这里插入图片描述

3.线程一开始扩容,注意这句关键代码:

Entry<K,V> next = e.next;

假如线程一执行完这句代码时,就被挂起了,此时线程一transfer()方法中:e = entry1, next = entry0, entry0.next = null。

4.巧了,此时线程二也开始触发扩容:
当线程二扩容完成之后,假如扩容后entry0和entry1也落到newTable的同一index位置,此时线程二中的newTable数据为:
在这里插入图片描述

注意 重点来了,线程二扩容完成后之后,ertry0的next指向了entry1。

5.回到线程一继续扩容:
注意,此时线程一transfer()方法中:e = entry1, next = entry0, entry0.next = entry1。当第一个元素entry1扩容完成之后,此时线程一中的newTable数据为:
在这里插入图片描述

此时 e = entry0, next = entry1, entry1.next = null。

6.线程一继续将e = entry0扩容copy到newTable中:
当元素entry0扩容完成之后,此时线程一中的newTable数据为:
在这里插入图片描述

此时 e = entry1, next = null。

7.线程一继续将e = entry1扩容到newTable中:
当元素entry1扩容完成之后,此时线程一中的newTable数据为:
在这里插入图片描述

注意 当这一轮扩容完成后,会把entry1.next = entry0,线程一扩容完成,同时线程二扩完容的时候,已经将entry0.next = entry1了,形成环形链表

8.当调用get()方法的时候,代码逻辑:

final Entry<K,V> getEntry(Object key) {
    if (size == 0) {
        return null;
    }
    int hash = (key == null) ? 0 : hash(key);
     // 问题就在这里,当key进行hash运算之后,刚好落在环形链表这个位置,
     // 当key没有匹配,会向下一直循环e.next,然后就在环形链表中出不来了。
    for (Entry<K,V> e = table[indexFor(hash, table.length)];
        e != null;
        e = e.next) {
        Object k;
        if (e.hash == hash &&
            ((k = e.key) == key || (key != null && key.equals(k))))
            return e;
    }
    return null;
}
### 关于 Java HashMap 环形链表问题及其解决方案 #### JDK7 中 HashMap 链表成环的原因 在 JDK7 的实现中,`HashMap` 使用的是 **头插法** 将新节点插入到链表头部。当多个线程同时操作同一个 `HashMap` 时,可能会发生竞态条件(race condition),从而导致链表形成环状结构[^1]。 具体来说,在多线程环境下,如果一个线程正在执行扩容操作,而另一个线程也在尝试插入新的键值对,则可能导致以下情况: - 头插法使得每次插入的新节点都会成为链表的第一个节点。 - 当多个线程并发修改同一链表时,由于缺乏同步机制,某些节点的 `next` 指针会被错误地设置,最终形成循环引用,即链表成环[^2]。 这种情况下,程序会陷入死循环,尤其是在调用 `size()` 或迭代器遍历 `HashMap` 时。 --- #### JDK8 中的改进与解决方案 JDK8 对 `HashMap` 进行了重要优化,解决了链表成环的问题。以下是其主要改进措施: 1. **尾插法替代头插法** - 在 JDK8 中,`HashMap` 改用了 **尾插法** 插入新节点,即将新节点追加到链表的末尾而不是头部。这样可以有效防止因头插法引发的环形链表问题[^3]。 2. **局部变量保护链表状态** - 扩容过程中,JDK8 引入了两个局部变量 `loHead` 和 `hiHead` 来分别存储旧链表和新链表的部分节点。这些局部变量仅存在于当前方法的作用域内,因此不会被其他线程干扰,进一步提高了线程安全性[^4]。 3. **引入红黑树结构** - 当链表长度超过一定阈值(默认为 8)时,JDK8 会将链表转换为红黑树以提高查找效率。这一改动虽然不直接影响环形链表问题,但在一定程度上减少了链表过长带来的性能开销。 4. **增强的并发控制** - 虽然 `HashMap` 并不是线程安全的数据结构,但 JDK8 的设计更加注重减少竞争条件的影响。例如,通过更精细的操作划分以及合理的锁粒度调整,降低了多线程环境下的冲突概率。 --- #### 示例代码对比 ##### JDK7 实现(存在隐患) ```java // 假设这是 JDK7 下的 put 方法部分逻辑 if (bucket[k] != null) { Node<K, V> newNode = new Node<>(hash, key, value, bucket[k]); bucket[k] = newNode; // 头插法直接覆盖原有节点 } ``` 上述代码展示了头插法的特点:新节点总是替换掉原链表的第一个节点,容易造成指针混乱。 --- ##### JDK8 实现(改进后的尾插法) ```java // 假设这是 JDK8 下的 putVal 方法部分逻辑 Node<K,V> p = tab[i]; if (p == null) tab[i] = newNode; else { binCount = 1; for (Node<K,V> e = p; ; ) { K ek; if (e.hash == hash && ((ek = e.key) == key || (key != null && key.equals(ek)))) { oldVal = e.value; if (!onlyIfAbsent) e.value = value; break; } Node<K,V> pred = e; if ((e = e.next) == null) { // 到达链表末端 pred.next = newNode; // 新节点追加至链表末尾 break; } } } ``` 可以看到,JDK8 的实现确保了新节点始终位于链表的最后一位,避免了头插法可能引发的环形链表风险。 --- ### 结论 综上所述,JDK7 存在链表成环的主要原因是采用了头插法并缺乏足够的线程安全保障;而在 JDK8 中,通过改用尾插法、引入局部变量以及优化扩容流程等方式成功消除了该问题。对于需要高并发支持的应用场景,建议优先考虑使用线程安全的集合类(如 `ConcurrentHashMap`),或者采取外部同步手段来保障数据一致性。 --- ####
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值