HashMap进行put操作会引起死循环?

探讨了在多线程环境下HashMap可能导致死循环的原因,分析了JDK1.8前后的实现差异,以及ConcurrentHashMap如何解决这一问题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

HashMap进行put操作会引起死循环?

最近在磕《java并发编程艺术》,在看到第六章的时候出现了下面这段我不是很理解的东西,如下

《java并发编程艺术》截取

为什么要使用ConcurrentHashMap

在并发编程中使用HashMap可能导致程序死循环。而使用线程安全的HashTable效率又非常低下,基于以上两个原因,便有了ConcurrentHashMap的登场机会。

1. 线程不安全的HashMap

在多线程环境下,使用HashMap进行put操作会引起死循环,导致CPU利用率接近100%,所以在并发情况下不能使用HashMap。例如,执行以下代码会引起死循环。

final HashMap<String, String> map = new HashMap<> (2);
        Thread t = new Thread (() -> {
            for (int i = 0; i < 10000; i++) {
                new Thread (() -> map.put (UUID.randomUUID ().toString (), ""), "ftf" + i).start ();
            }
        }, "ftf");
        t.start();
        t.join();

HashMap在并发执行put操作时会引起死循环,是因为多线程会导致HashMap的Entry链表形成环形数据结构,一旦形成环形数据结构,Entry的next节点永远不为空,就会产生死循环获取Entry。

不知道是不是由于篇幅原因,我觉得作者并没有把为什么会引起死循环说明白,我只好自己再去看源码了

JDK1.8 HashMap源码
HashMap中关于put操作的源码如下
public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
}
 final V putVal (int hash, K key, V value, boolean onlyIfAbsent,
                    boolean evict) {
        Node<K, V>[] tab;
        Node<K, V> p;
        int n, i;
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize ()).length;
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode (hash, key, value, null);
        else {
            Node<K, V> e;
            K k;
            if (p.hash == hash &&
                    ((k = p.key) == key || (key != null && key.equals (k))))
                e = p;
            else if (p instanceof TreeNode)
                e = ((TreeNode<K, V>) p).putTreeVal (this, tab, hash, key, value);
            else {
                for (int binCount = 0; ; ++ binCount) {
                    //e是p的下一个节点
                    if ((e = p.next) == null) {
                        //插入链表的尾部
                        p.next = newNode (hash, key, value, null);
                        //如果插入后链表长度大于8则转化为红黑树
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin (tab, hash);
                        break;
                    }
                    //如果key在链表中已经存在,则退出循环
                    if (e.hash == hash &&
                            ((k = e.key) == key || (key != null && key.equals (k))))
                        break;
                    p = e;
                }
                //如果key在链表中已经存在,则修改其原先的key值,并且返回老的值
                if (e != null) { // existing mapping for key
                    V oldValue = e.value;
                    if (! onlyIfAbsent || oldValue == null)
                        e.value = value;
                    afterNodeAccess (e);
                    return oldValue;
                }
            }
            ++ modCount;
            if (++ size > threshold)
                resize ();
            afterNodeInsertion (evict);
            return null;
}

当你看到这的时候是不是还是不懂为什么会有死循环?是的,我看到这也没懂,只能去网上看看大佬们怎么说

百度上关于这个问题的第一篇文章就是这个:https://www.cnblogs.com/wfq9330/p/9023892.html

这源码跟我看的JDK1.8的截然不同,目测应该是JDK1.8以下的

结论
  1. JDK1.8之前,为了提高rehash的速度,冲突链表是使用头插法,因为头插法是操作速度最快的,找到数组位置就直接找到插入位置了,头插法在多线程下回引起死循环
  2. JDK1.8之后开始加入红黑树,当链表长度大于8时链表就会转换成红黑树,这样就大大提高了在冲突链表查找的速度,同时因为链表的长度不可能大于8,链表在rehash的消耗就小很多,所以JDK1.8使用尾插法也避免了死循环问题
<think>嗯,用户想了解Java HashMap的内部实现和底层数据结构。我记得HashMap在Java中挺重要的,尤其是在面试中经常被问到。首先,我得回忆一下HashMap的基本结构。 首先,HashMap是基于哈希表实现的,对吧?哈希表通常由数组和链表组成。不过在Java 8之后,当链表过长时,会转换成红黑树,这样能提高查询效率。这点应该要提到,用户可能知道红黑树的存在,但具体细节可能不清楚。 然后,哈希冲突的处理方法。HashMap用的是链地址法,也就是数组加链表的形式。当不同的键产生相同的哈希值时,会在同一个数组位置形成链表。但链表过长的话,查询性能会下降,所以Java 8引入了红黑树,当链表长度超过8时,链表转为红黑树,而元素减少到6时,又会转回链表。这个阈值需要准确说明,不能搞错。 接下来是哈希函数。HashMap如何计算键的哈希值呢?我记得在Java 8中,hash()方法将键的hashCode的高16位和低16位进行异或运算,以减少哈希冲突。而之前的版本可能扰动次数更多,比如Java 7的hash方法有四次扰动,性能稍差。这一点可能需要对比说明,引用中的信息提到过Java 7和8的hash方法不同,用户可能想了解优化点。 扩容机制也是重点。默认初始容量是16,负载因子0.75,当元素数量超过容量乘以负载因子时,会扩容为原来的两倍。扩容时,元素会重新计算位置,要么在原索引,要么在原索引加上旧容量的位置。这里需要解释为什么是两倍扩容,以及如何高效地重新散列。引用中提到HashMap总是使用2的幂作为容量,这样在计算索引时可以用位运算代替取模,提高效率。这点很重要,要解释清楚。 线程安全方面,HashMap不是线程安全的,多线程环境下可能造成死循环或者数据不一致。可以使用ConcurrentHashMap或者Collections.synchronizedMap来保证线程安全。用户可能在引用中看到过相关说明,需要提到这一点,并给出解决方案。 底层数据结构的变化,比如从数组+链表到数组+红黑树的转换,以及为什么要这样做。红黑树的自平衡特性可以在最坏情况下保持O(log n)的时间复杂度,而链表是O(n)。这解释了性能优化的原因。可能用户还关心为什么阈值是8和6,这涉及到统计学上的泊松分布,不过可能不需要深入,但可以简要说明。 另外,put和get方法的流程也需要描述。比如,put时计算哈希,找到数组位置,处理冲突,可能触发树化;get时同样计算哈希,遍历链表或树。这帮助用户理解内部操作步骤。 可能用户还关心HashMap允许null键和null值,而HashTable不允许,这也是一个区别点。不过问题主要在于内部结构,这点可能次要,但可以稍微提一下。 总结起来,回答的结构应该是先总体介绍,然后分点详细说明底层结构、哈希函数、扩容机制、线程安全等,每个部分结合引用中的信息,比如红黑树的引入、容量为2的幂、不同版本的哈希函数差异等。确保引用标识正确添加到相关段落末尾,保持回答的准确性和引用来源的可信度。 用户可能没有直接问及线程安全或优化方法,但根据引用中的信息,这些问题可能作为相关问题提出,需要提前准备。例如,如何解决HashMap线程不安全的问题,或者为什么JDK8引入红黑树。所以在相关问题部分,可以包括这些点,帮助用户扩展知识。 最后,检查是否符合格式要求:使用中文,数学表达式用$...$,独立公式用$$...$$,代码块正确,引用标识自然添加。确保LaTeX语法正确,比如下划线和特殊符号的转义。</think>### Java HashMap 内部实现及底层数据结构 #### 1. 底层数据结构 Java HashMap 的底层数据结构在 JDK1.8 之后从 **数组 + 链表** 改进为 **数组 + 链表/红黑树**。 - **数组**:用于存储哈希桶(`Node<K,V>[] table`),每个桶对应一个哈希值区间。 - **链表**:当哈希冲突时,冲突的键值对以链表形式存储在同一个桶中。 - **红黑树**:当链表长度超过阈值(默认 8)时,链表会转换为红黑树,以提高查询效率(时间复杂度从 $O(n)$ 优化为 $O(\log n)$)[^4]。 #### 2. 哈希函数 HashMap 通过 `hash()` 方法计算键的哈希值: ```java static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); } ``` 该方法将 `key.hashCode()` 的高 16 位与低 16 位进行异或运算,以减少哈希冲突[^5]。 #### 3. 哈希冲突解决 采用 **链地址法**: - 插入时,若桶为空,直接存储新节点; - 若桶非空,遍历链表/红黑树,通过 `equals()` 方法判断键是否重复。 - 若链表长度超过 8 且数组长度 ≥ 64,链表转换为红黑树;若红黑树节点数 ≤ 6,则退化为链表[^4]。 #### 4. 扩容机制 - **默认初始容量**:16,**负载因子**:0.75。 - **扩容条件**:当元素数量 > 容量 × 负载因子时,容量扩大为 2 倍。 - **重新散列**:扩容后,元素需重新计算索引。由于容量是 2 的幂,新索引为原位置或原位置 + 旧容量(利用 `(e.hash & oldCap) == 0` 判断)[^2]。 #### 5. 线程安全性 HashMap **非线程安全**,多线程操作可能导致死循环(JDK1.7 链表头插法引起)或数据不一致。建议使用 `ConcurrentHashMap` 或通过 `Collections.synchronizedMap` 包装[^3]。 #### 6. 代码示例(简化版 put 流程) ```java final V putVal(int hash, K key, V value, boolean onlyIfAbsent) { Node<K,V>[] tab; Node<K,V> p; int n, i; if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); else { // 处理哈希冲突(链表或红黑树) } if (++size > threshold) resize(); return null; } ``` #### 7. 性能优化 - **红黑树**:降低最坏情况下的时间复杂度。 - **哈希函数优化**:JDK1.8 的扰动次数减少,但分布更均匀。 - **容量为 2 的幂**:通过位运算 `(n-1) & hash` 替代取模运算,提升效率[^2]。 ---
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值