一、死环问题:JDK1.7的"致命陷阱"
问题背景 :
在JDK1.7的HashMap中,多线程并发扩容时可能因头插法导致链表反转,形成死循环。以下是其核心过程:
- 触发条件 :两个线程同时执行
resize()
,且原链表有多个节点(如A→B→C)。 - 反转链表:线程1执行头插法,将链表变为C→B→A;此时线程2开始遍历,若此时CPU切换至线程2,其仍持有旧链表的引用(A→B→C)。
- 循环形成:线程2将A插入新链表头部,接着处理B时,B.next指向A,形成A↔B的循环。
代码片段(JDK1.7):
java
void transfer(Entry[] newTable) {
for (Entry<K,V> e : table) {
while (e != null) {
Entry<K,V> next = e.next; // 线程切换点
e.next = newTable[i]; // 头插法
newTable[i] = e;
e = next;
}
}
}
解决方案 :
JDK1.8改用尾插法 ,保持链表顺序,避免反转,但并发操作仍可能导致数据丢失,因此需用ConcurrentHashMap
。
二、面试官视角:HashMap核心考察点
1. 数据结构演进
- JDK1.7:数组 + 链表(碰撞时链表头插)。
- JDK1.8:数组 + 链表/红黑树(链表长度≥8且桶数量≥64时树化,树节点≤6时退化为链表)。
2. put/get方法实现
-
put流程:
- 计算
key.hashCode()
,二次哈希(扰动函数)降低碰撞概率。 - 确定桶位置:
(n-1) & hash
。 - 处理碰撞:
- 链表:遍历至尾节点插入(JDK1.8尾插)。
- 红黑树 :调用
TreeNode.putTreeVal()
。
- 扩容检查:若当前size≥阈值(容量×负载因子),触发
resize()
。
- 计算
-
get流程:
- 计算哈希定位桶。
- 遍历链表或红黑树查找匹配节点。
3. JDK1.7 vs JDK1.8并发设计差异
-
JDK1.7 ConcurrentHashMap:
- Segment分段锁 :每个Segment继承
ReentrantLock
,锁粒度较粗。 - 锁分离:不同Segment的写操作可并行。
- Segment分段锁 :每个Segment继承
-
JDK1.8 ConcurrentHashMap:
- synchronized + CAS:锁粒度细化到桶头节点,CAS用于无锁化初始化、计数等操作。
- 弃用ReentrantLock的原因 :
- 内存开销 :
ReentrantLock
基于AQS,每个锁对象占用更多内存。 - JVM优化:synchronized在JDK1.6后引入锁升级(偏向锁→轻量锁→重量锁),性能接近ReentrantLock。
- 内存开销 :
4. 扩容机制与阈值设计
-
负载因子(默认0.75):权衡空间利用率与哈希碰撞概率。
-
扩容阈值 :
容量 × 负载因子
。 -
树化条件:
- 链表长度≥8且桶数量≥64 → 转为红黑树。
- 桶数量<64 → 优先扩容而非树化。
-
扩容优化(JDK1.8):
- 高位掩码法 :节点在新表中的位置为
原位置
或原位置 + 旧容量
,避免重新哈希。 - 链表拆分:保持顺序,避免死环。
- 高位掩码法 :节点在新表中的位置为
三、高频面试考点与设计哲学
1. 时间复杂度分析
- 理想情况:O(1)(直接命中桶)。
- 链表冲突:O(n)。
- 红黑树冲突:O(log n)。
2. 为什么选择红黑树而非AVL树?
- 平衡性妥协:红黑树插入/删除时旋转次数更少,适合频繁写操作的场景。
3. 哈希扰动函数的作用
-
JDK1.8的hash() :
javastatic final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }
- 高位参与运算:将高16位异或到低16位,减少哈希碰撞概率。
4. 线程安全问题
- 数据覆盖:并发put时,若两个线程同时发现桶为空,可能导致后写入的值覆盖前值。
- 扩容数据丢失:并发扩容时节点未正确迁移。
5. 为什么允许null键/值?
- 设计选择 :HashMap明确允许null,而
ConcurrentHashMap
禁止null(避免二义性)。
四、总结:HashMap的设计智慧
- 空间换时间:通过负载因子和扩容机制平衡空间占用与查询效率。
- 渐进式优化:从链表到红黑树,避免极端情况下的性能劣化。
- 并发取舍:在单线程性能与多线程安全之间,HashMap选择前者,强调正确使用场景(如用ConcurrentHashMap替代)。
- 算法与工程结合:哈希扰动、树化阈值等细节均基于统计学与工程实践。