面试中HashMap链表成环的问题你答出了吗

​ HashMap作为老生常谈的问题,备受面试官的青睐,甚至成为了面试必问的问题。由于大量的针对HashMap的解析横空出世,面试官对HashMap的要求越来越高,就像面试官对JVM掌握要求越来越高一样,今天我们来研究下HashMap的链表环化的问题,你知道其中的原理嘛?关注公众号“程序员清辞”,获取更多内容

​ 在JDK1.7版本下,有个线程安全的问题,经常会被问到,很多求职者可能还在对比Hashtable线程安全性,其实面试官想得到的链表成环造成线程安全的问题,而这个问题在JDK1.8中已经得到了解决,但至于出现这样问题的原因,我翻看了很多帖子,大家剖析的很透彻,但是很难理解,今天结合自己的研究利用一篇帖子来阐述其中的奥秘。

JDK1.7扩容源码解析

​ 首先我来了解下HashMap中经典的扩容代码,回顾下扩容的过程

public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable { 
    
    //......
    
    // 扩容方法
    void resize(int newCapacity) {
        // 1、创建临时变量,将HashMap数组数据赋值给新数组作临时存储
        Entry[] oldTable = table;
        // 2、判断老数组长度是否超过了允许的最大长度,最大长度为 1 << 30
        int oldCapacity = oldTable.length;
        if (oldCapacity == MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return;
        }
		// 3、创建新的Entry数组,并扩容
        Entry[] newTable = new Entry[newCapacity];
        // 4、扩容赋值,即将老数组中的数据赋值到新数组中
        // initHashSeedAsNeeded(newCapacity) 得到的是一个hash的随机值(哈希种子),在计算哈希码时会用到这个种子,作用是减少哈希碰撞
        transfer(newTable, initHashSeedAsNeeded(newCapacity));
        // 6、扩容后赋值
        table = newTable;
        threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
    }
    
    // newTable : 表示新数组,即扩容后创建的新数组
    // rehash : 是否需要重新计算哈希值
    void transfer(Entry[] newTable, boolean rehash) {
		int newCapacity = newTable.length;
        // 5、将老map中的数据赋值到新map中(数组和链表复制迁移)
		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);
				}
                // 计算Entry元素在Entry[]数组中的位置
				int i = indexFor(e.hash, newCapacity);

				// 链表头插法赋值过程
				e.next = newTable[i];
				newTable[i] = e;
				e = next;
			}
		}
	}
    
    //......
    
}
  1. 创建临时变量,将HashMap数组数据赋值给新数组作临时存储
  2. 判断老数组长度是否超过了允许的最大长度,最大长度为 1 << 30
  3. 创建新的Entry数组,并扩容
  4. 扩容赋值,即将老数组中的数据赋值到新数组中
  5. 将老map中的数据赋值到新map中(数组和链表复制迁移)
  6. 扩容后赋值

链表迁移过程

以下三行代码描述了链表头插的整个过程,下面来剖析下这个过程:

e.next = newTable[i];
newTable[i] = e;
e = next;

关注公众号“程序员清辞”,获取更多内容
假设HashMap的存储状态如下:
在这里插入图片描述
​ e为数组位置的元素,e1、e2为e下形成的链表,h为将要赋值的位置,箭头代表链表指向

e.next = newTable[i]

​ 对oldTable进行遍历的过程中,取出元素e,假设先取出图中的元素e,在执行这行代码时,相当于断开x位置e与e1的链表关系,并与newTable[i]建立链表关系,此时newTable[i]位置为null
在这里插入图片描述

newTable[i] = e

​ 此时将oldTable中的e复制到newTable中的i位置,同时链表e指向null
在这里插入图片描述
​ 问题:那oldTable中e1和e2形成的链表怎么办?

​ 其实在之前的代码中已经阐述了,详情如下:

while(null != e) {

	// 这里已经将e.next存储为一个临时变量,也就是e1和e2形成的链表
	Entry<K,V> next = e.next;
	if (rehash) {
		e.hash = null == e.key ? 0 :hash(e.key);
	}
	......
}		

e = next;

​ 将next的值赋值给e,这行代码对上述的链表没有实质的影响,并且这已经是while循环的最后一行代码了,这行代码的目的是为下一次while遍历过程能从e1元素开始,而不是e,因为此时需要的遍历的e已经变成了e1。
通过这次数据迁移可能没有得到比较有参考意义的分析,所以我们需要再进行一次遍历分析,而这次遍历分析从e1开始。这里就不详细阐述,直接上图。

e.next = newTable[i]
在这里插入图片描述

newTable[i] = e
在这里插入图片描述
最终效果
公众号“程序员清辞”,获取更多内容
在这里插入图片描述
​ 以上就是整个数据迁移的过程,通过链表实例大家发现HashMap利用头插法完成迁移的过程,下面进入重点,链表成环

并发操作链表成环

产生基本条件

  1. 多线程环境并发操作
  2. HashMap扩容时候发生

问题解析

​ 在多线程环境下,a,b两个线程同时操作这个HashMap,由于HashMap是线程不安全的,假如线程a已经完成以上全过程,也就是下图
在这里插入图片描述
​ 代码执行到如下位置,还没有完全的出栈
在这里插入图片描述
此时线程b同时也在遍历这条链表,同时代码运行到while循环位置
在这里插入图片描述
这时线程b已经重新获取e数据时,由于a线程的操作还没有将数据同步到主内存,导致出现如下情况:
在这里插入图片描述

问题总结

  1. 插入的时候和平时我们追加到尾部的思路是不一致的,是链表的头结点开始循环插入,导致插入的顺序和原来链表的顺序相反的。
  2. table 是共享的,table 里面的元素也是共享的,while 循环都直接修改 table 里面的元素的 next 指向,导致指向混乱。
<think>好的,我现在需要处理用户关于HashMap面试题目的查询。首先,用户已经提供了四个引用来源,其中引用[1]、[2]、[3]、[4]都涉及Java集合框架,特别是HashMap的相关内容。我需要根据这些引用来整理出常见的问题和答案。 首先,我需要回顾每个引用的内容。引用[1]提到了TreeSet、区分重复元素的方法、ArrayList和Vector的区别,以及Collection和Collections的区别。引用[2]讨论了HashMap与Hashtable的区别,以及可能的刁钻问题。引用[3]详细描述了JDK1.8前后HashMap的底层实现,包括扰动函数、拉链法和红黑树。引用[4]则是关于Java集合的20个问题,但具体内容未详细列出,不过可能包含更多相关问题。 用户的需求是查找HashMap面试题及答案,因此需要重点从引用[2]、[3]中提取信息,并结合其他引用中的相关内容。例如,引用[3]详细解释了HashMap的底层结构、哈希冲突解决方法和红黑树的引入,这些都是常见的面试问题。引用[2]提到了HashMap与Hashtable的区别,这也是经典问题。 接下来,我需要整理出常见的问题,并确保每个问题都有对应的解答,同时正确引用来源。例如,问题可能包括: 1. HashMap和Hashtable的区别?答案应涉及null键值、同步性等,引用[2]和[3]。 2. JDK1.8中HashMap的改进?涉及红黑树,引用[3]。 3. 哈希冲突解决方法?拉链法和红黑树,引用[3]。 4. 扰动函数的作用?引用[3]。 5. 为何HashMap不保证顺序?引用[3]或常识。 6. Collection和Collections的区别?引用[1]。 7. 其他相关问题如初始容量、加载因子、线程安全等,可能需综合多个引用。 需要注意每个答案后正确标注引用来源,例如关于扰动函数的部分来自引用[3]。同时,用户要求使用中文回答,行内公式用$...$,独立公式用$$...$$,但根据问题内容可能公式较少,但需注意LaTeX语法正确。 另外,用户示例中的快速排序部分有代码块,这里可能需要针对HashMap问题加入代码示例,如扰动函数的实现,或者红黑树相关的伪代码,但需确认引用内容中是否有提供。例如,引用[3]提到扰动函数是HashMap的hash方法,可以举例代码。 最后,生相关问题部分需要基于已整理的问题,提出扩展性问题,例如性能优化、并发处理等,可能需要结合引用中的内容或常识。 现在需要确保每个问题解答准确,引用正确,格式符合要求,并且结构清晰。可能还需要检查是否有重复问题,或者是否覆盖了用户关心的主要方面。</think>### HashMap面试问题及解答 #### 1. HashMap与Hashtable的区别是什么? - **HashMap**允许键和值为`null`,而**Hashtable**不允许[^2]。 - **HashMap**是非线程安全的(非synchronized),而**Hashtable**的方法通过`synchronized`关键字保证线程安全[^2][^3]。 - **HashMap**在JDK1.8后引入红黑树优化链表过长的问题,而**Hashtable**无此机制。 #### 2. JDK1.8中HashMap的底层实现有哪些改进? JDK1.8前,HashMap使用**数组+链表**(拉链法)处理哈希冲突;JDK1.8后,当链表长度超过阈值(默认为8)时,链表会转换为**红黑树**,以提高查询效率(时间复杂度从$O(n)$降为$O(\log n)$)。 #### 3. HashMap如何解决哈希冲突? - **拉链法**:将哈希值相同的键值对存储在链表中。 - **红黑树优化**:当链表过长时转换为红黑树(JDK1.8引入)。 #### 4. 什么是扰动函数?它的作用是什么? 扰动函数是HashMap的`hash()`方法,通过将键的哈希码的高16位与低16位异或,减少哈希冲突。例如: ```java static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); } ``` 其目的是让哈希码分布更均匀,减少低质量`hashCode()`实现的影响。 #### 5. HashMap的初始容量和加载因子是什么? - **初始容量**默认为16,**加载因子**默认为0.75。当元素数量超过`容量×加载因子`时触发扩容(容量翻倍)。 - 公式:扩容阈值$T = \text{capacity} \times \text{loadFactor}$。 #### 6. 为何HashMap不保证元素的顺序? 因为哈希函数将键分散到不同的桶中,且扩容时可能重新哈希元素,导致顺序变化。 #### 7. Collection和Collections的区别是什么? - **Collection**是集合框架的根接口(如List、Set)。 - **Collections**是工具类,提供静态方法操作集合(如排序、同步包装)[^1]。 #### 8. HashMap为何线程不安全? 多个线程同时扩容可能导致链表或数据丢失。线程安全替代方案: - 使用`ConcurrentHashMap`。 - 通过`Collections.synchronizedMap()`包装。 #### 9. HashMap的`put()`方法流程? 1. 计算键的哈希值。 2. 定位到数组索引。 3. 若桶为空,直接插入;否则遍历链表/红黑树: - 键存在则更新值。 - 键不存在则插入新节点。 4.链表长度≥8且数组长度≥64,链表转红黑树。 #### 10. 为何JDK1.8引入红黑树而非其他数据结构? 红黑树在增删查操作上综合性能较好(平均时间复杂度$O(\log n)$),且能避免链表过长导致的性能骤降。 --- ###
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值