HashMap中是如何形成环形链表的

探讨Java HashMap在多线程环境下如何形成环形链表,解析其内部扩容机制及线程安全问题。

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

导读:经过前面的博客总结,可以知道的是,HashMap是有一个一维数组和一个链表组成,从而得知,在解决冲突问题时,hashmap选择的是链地址法。为什么HashMap会用一个数组这链表组成,当时给出的答案是从那几种解决冲突的算法中推论的,这里给出一个正面的理由:

1,为什么用了一维数组:数组存储区间是连续的,占用内存严重,故空间复杂的很大。但数组的二分查找时间复杂度小,为O(1);数组的特点是:寻址容易,插入和删除困难

2,为什么用了链表:链表存储区间离散,占用内存比较宽松,故空间复杂度很小,但时间复杂度很大,达O(N)。链表的特点是:寻址困难,插入和删除容易

而HashMap是两者的结合,用一维数组存放散列地址,以便更快速的遍历;用链表存放地址值,以便更快的插入和删除!

一、环形链表的形成分析
那么,在HashMap中,到底是怎样形成环形链表的?这个问题,得从HashMap的resize扩容问题说起!

备注:本博客中所示源码,均为java 7版本

HashMap的扩容原理:

 /**
     * The default initial capacity - MUST be a power of two.
     */
    static final int DEFAULT_INITIAL_CAPACITY = 16;
 
    /**
     * The maximum capacity, used if a higher value is implicitly specified
     * by either of the constructors with arguments.
     * MUST be a power of two <= 1<<30.
     */
    static final int MAXIMUM_CAPACITY = 1 << 30;
 
    /**
     * The load factor used when none specified in constructor.
     */
    static final float DEFAULT_LOAD_FACTOR = 0.75f;

当HashMap中的元素个数超过数组大小(数组总大小length,不是数组中个数size)*loadFactor时,就会进行数组扩容,loadFactor的默认值为0.75,这是一个折中的取值。也就是说,默认情况下,数组大小为16,那么当HashMap中元素个数超过16*0.75=12(这个值就是代码中的threshold值,也叫做临界值)的时候,就把数组的大小扩展为 2*16=32,即扩大一倍,然后重新计算每个元素在数组中的位置,而这是一个非常消耗性能的操作,所以如果我们已经预知HashMap中元素的个数,那么预设元素的个数能够有效的提高HashMap的性能。
再看源码中,关于扩容resize()的实现:

   /**
     * Rehashes the contents of this map into a new array with a
     * larger capacity.  This method is called automatically when the
     * number of keys in this map reaches its threshold.
     *
     * If current capacity is MAXIMUM_CAPACITY, this method does not
     * resize the map, but sets threshold to Integer.MAX_VALUE.
     * This has the effect of preventing future calls.
     *
     * @param newCapacity the new capacity, MUST be a power of two;
     *        must be greater than current capacity unless current
     *        capacity is MAXIMUM_CAPACITY (in which case value
     *        is irrelevant).
     */
    void resize(int newCapacity) {
        Entry[] oldTable = table;
        int oldCapacity = oldTable.length;
        if (oldCapacity == MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return;
        }
 
        Entry[] newTable = new Entry[newCapacity];
        boolean oldAltHashing = useAltHashing;
        useAltHashing |= sun.misc.VM.isBooted() &&
                (newCapacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
        boolean rehash = oldAltHashing ^ useAltHashing;
        transfer(newTable, rehash);
        table = newTable;
        threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
    }

备注:请注意这句话: newCapacity the new capacity, MUST be a power of two; must be greater than current capacity unless current  capacity is MAXIMUM_CAPACITY (in which case value is irrelevant)
在这里面,又调用了一个函数transfer函数:

   /**
     * Transfers all entries from current table to newTable.
     */
    void transfer(Entry[] newTable, boolean rehash) {
        int newCapacity = newTable.length;
        for (Entry<K,V> e : table) {
            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.next = newTable[i];
                newTable[i] = e;
                e = next;
            }
        }
    }
总得来说,就是拷贝旧的数据元素,从新新建一个更大容量的空间,然后进行数据复制!
那么关于环形链表的形成,则主要在这扩容的过程。当多个线程同时对这个HashMap进行put操作,而察觉到内存容量不够,需要进行扩容时,多个线程会同时执行resize操作,而这就出现问题了,问题的原因分析如下:

首先,在HashMap扩容时,会改变链表中的元素的顺序,将元素从链表头部插入。PS:说是为了避免尾部遍历,这一部分不是本博客的主要介绍内容,后面再说。

而环形链表就在这一时刻发生,以下模拟2个线程同时扩容。假设,当前hashmap的空间为2(临界值为1),hashcode分别为0和1,在散列地址0处有元素A和B,这时候要添加元素C,C经过hash运算,得到散列地址为1,这时候由于超过了临界值,空间不够,需要调用resize方法进行扩容,那么在多线程条件下,会出现条件竞争,模拟过程如下:

(额,我画图的功底一向不好,见谅见谅)

线程一:读取到当前的hashmap情况,在准备扩容时,线程二介入

线程二:读取hashmap,进行扩容

线程一:继续执行

这个过程为,先将A复制到新的hash表中,然后接着复制B到链头(A的前边:B.next=A),本来B.next=null,到此也就结束了(跟线程二一样的过程),但是,由于线程二扩容的原因,将B.next=A,所以,这里继续复制A,让A.next=B,由此,环形链表出现:B.next=A; A.next=B 

二、总结
在这里,只总结一个事儿,额,算是摘抄总结吧,就是在源码注释中,发现扩容的时候,必须为2的指数,这是为什么呢?

请点击此链接:HashMap扩容机制、线程安全  或者,自行学习hashmap的扩容机制

本篇博客介绍环形链表的形成就先到这里,下一篇博客介绍怎么判断是否出现环形链表!
--------------------- 
作者:~哀而不伤 
来源:优快云 
原文:https://blog.youkuaiyun.com/hhx0626/article/details/54024222 
版权声明:本文为博主原创文章,转载请附上博文链接!

<think>好的,我现在需要处理用户关于Java HashMap的面试题及答案的查询。首先,用户提供了几个引用资料,我需要仔细阅读这些资料,提取关键信息,然后组织成结构清晰的回答。 首先,引用[1]提到HashMap是Java中重要且常用的数据结构,面试中常被问到,并列举了一些经典问题,比如工作原理。引用[2]和[3]都强调了HashMap在面试中的高频出现,通常会出现在前几题,考察候选人对JDK源码的理解。引用[4]详细列出了多个面试题,包括数据结构、哈希冲突处理、扩容机制等。引用[5]则给出了回答HashMap底层实现时的要点,如初始容量、负载因子、红黑树转换等。 接下来,我需要根据这些信息整理出常见的面试题及答案。要注意用户提到的系统级指令,比如行内数学公式用$...$,独立公式用$$...$$,但这里可能用不到数学公式。不过需要确保回答结构清晰,分点列出问题和答案,并正确添加引用标识。 可能的面试题包括: 1. HashMap的工作原理? 2. 如何处理哈希冲突? 3. 扩容机制是怎样的? 4. 为什么使用红黑树? 5. HashMap和HashTable的区别? 6. 初始容量和负载因子的作用? 7. 哈希函数如何实现? 8. 线程安全问题? 9. 与ConcurrentHashMap的区别? 10. 使用场景? 对于每个问题,需要结合引用中的内容给出简明扼要的回答,并在末尾添加引用标记。例如,工作原理涉及数组+链表+红黑树的结构,哈希冲突通过链表和红黑树解决,扩容机制涉及重新计算索引等。 需要确保答案准确,引用正确的来源,比如引用[4]提到jdk7和jdk8的结构变化,引用[5]提到初始容量和负载因子。同时,注意回答要符合中文表达,避免技术术语使用错误。 最后,生成相关问题,可能包括优化方法、性能分析、具体源码实现等,确保问题与主题相关,帮助用户进一步学习。</think>### Java HashMap 常见面试题及答案 #### 1. HashMap 的底层数据结构是什么? JDK 1.7 及之前使用**数组+链表**,JDK 1.8 引入**数组+链表+红黑树**。当链表长度超过阈值(默认8)时,链表转换为红黑树以提升查询效率[^4]。 #### 2. HashMap 如何处理哈希冲突? - 通过**链地址法**(链表)解决冲突,即哈希值相同的键值对存储在同一个数组位置的链表中。 - JDK 1.8 后,当链表长度超过阈值,链表转换为红黑树,将查询时间复杂度从$O(n)$降至$O(\log n)$。 #### 3. HashMap 的扩容机制是什么? - 默认初始容量为16,负载因子为0.75。当元素数量超过$容量 \times 负载因子$时,触发扩容: - 容量扩大为原来的2倍。 - 键值对重新计算索引并分配到新数组中[^5]。 #### 4. 为什么 JDK 1.8 引入红黑树? 链表过长会导致查询效率降低,红黑树通过平衡二叉树的特性,将最坏情况下的时间复杂度从$O(n)$优化为$O(\log n)$[^4]。 #### 5. HashMap 和 HashTable 的区别? | **HashMap** | **HashTable** | |---------------------------|----------------------------| | 线程不安全 | 线程安全(方法加 synchronized 锁) | | 允许键/值为 null | 不允许键/值为 null | | 默认初始容量16 | 默认初始容量11 | | JDK 1.2 引入 | JDK 1.0 引入 | [^4] #### 6. 初始容量和负载因子的作用? - **初始容量**:决定哈希数组的初始大小,减少扩容次数。 - **负载因子**:控制扩容阈值(容量 × 负载因子),权衡时间与空间效率(默认0.75)。 #### 7. 哈希函数如何实现? 键的哈希值通过`hashCode()`计算,再通过扰动函数(异或高16位与低16位)减少哈希冲突: ```java static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); } ``` 扰动函数使哈希分布更均匀[^4]。 #### 8. HashMap 为什么线程不安全? 多线程环境下,扩容时可能导致链表形成环形结构(JDK 1.7),或数据覆盖问题(JDK 1.8)。 #### 9. HashMap 和 ConcurrentHashMap 的区别? - **HashMap**:线程不安全,效率高。 - **ConcurrentHashMap**: - JDK 1.7 使用分段锁(Segment)。 - JDK 1.8 改用 CAS + synchronized 锁单个桶(Node),并发性能更高[^4]。 #### 10. HashMap 的应用场景? - 高频键值查询(如缓存)。 - 无需保证线程安全的场景。 - 需要快速插入、删除、查找(时间复杂度接近$O(1)$)。 --- §§ 1. HashMap 在多线程下如何避免死循环? 2. 如何设计一个自定义对象作为 HashMap 的键? 3. ConcurrentHashMap 的 size() 方法如何实现? 4. 为什么负载因子默认值是0.75? 5. HashMap 的键为 null 时存储在哪个位置? [^1]: 引用1 [^2]: 引用2 [^3]: 引用3 : 引用4 : 引用5
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值