HashMap 1.8 全面解析与面试速答
一、概览
- 底层结构:数组
Node<K,V>[] table+ 桶内链表(发生冲突时)+ 桶内红黑树(高冲突时)。 - JDK 1.8 关键升级点:
- 链表尾插(保持顺序,降低扩容期间产生环的风险),
- 引入红黑树优化高冲突桶的查找效率。
二、核心参数(必须记牢)
- 默认初始容量:
16 - 最大容量:
1 << 30 - 默认负载因子:
0.75f - 链表转红黑树阈值:
8 - 红黑树退化回链表阈值:
6 - 允许树化最小表容量:
64
三、定位桶的奥秘:hash 扰动与索引计算
// 1. 取 hash 值
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
// 2. 桶索引计算
index = (n - 1) & hash;
- 扰动函数:
h ^ (h >>> 16),用于混合高位,减少 hash 冲突。 - 下标计算:
(n - 1) & hash,n 为 table 长度且恒为 2 的幂,效率极高。
四、put 流程梳理
- 扰动 hash+定位槽位:提升分布均匀度,保证高效存取。
- table 初始化:若为空,resize() 初始化(默认16)。
- 槽位空/非空:空则直接插入,否则遍历桶(链表/树),已存在则覆盖 value,否则尾插新节点。
- 树化判定:桶内链表长
≥8且 table 长≥64,才树化。 - 扩容判定:当前元素数
≥容量 × 负载因子时自动扩容(容量翻倍)。
五、get 流程总结
- hash 扰动后,按索引定位桶。
- 桶为树,则树查找;为链表,则 equals 比较。
- 命中返回 value,未命中返回 null。
六、resize 扩容机制(1.8 优化)
- 触发点:
size > threshold - 步骤:
- 建立新表(容量双倍),阈值随之变化。
- 遍历旧桶,将节点依次拆为低/高位两组:
e.hash & oldCap == 0:原索引位置;e.hash & oldCap != 0:新索引 = 原索引 + oldCap。
- 保持链表相对顺序(尾插),避免成环。
- 树形桶可能分为两棵更小的树,或退化成链表。
优化要点:利用位运算完成节点拆分,迁移零额外 hash 计算,极高效率。
七、树化与退化规则
- 树化:单桶链表节点数
≥8且表长≥64才树化。 - 退化:树化桶节点减至
≤6时自动回退为链表,节约内存。
大表遇真冲突才树化,避免在小表时过早树化带来空间浪费。
八、为何容量必须为 2 的幂?
(n - 1) & hash高效取模;- 保证 hash 低位能均匀分布;
- 扩容节点无需重新 hash,直接通过
& oldCap分组,决定留原位or偏移,迁移 O(1)。
九、复杂度 & 空间分析
- 平均查找/插入:O(1) 摊还;
- 极端冲突:链表 O(k),树化后 O(log k);
- 树节点空间消耗高于链表。
十、线程安全问题(高频面试)
- 并发 put:同桶冲突可能数据被覆盖。
- 并发 resize:迁移中间态有概率丢节点/读不一致(1.7 更严重,1.8 尾插有所缓解)。
- 可见性:无同步,get 可能看到“未完全发布”的数据。
- 结论:多线程下 HashMap 绝不安全,强烈推荐用 ConcurrentHashMap!
十一、常见误区纠正
- 不是 put 时就树化,要满足“链表长≥8 && 表长≥64”。
(n-1)&hash能高效取模,前提是 n 一定为 2 的幂。- JDK1.8 并不线程安全,只是降低了头插成环概率。
十二、面试速答超浓缩(30s)
HashMap 1.8: 底层“数组+链表+红黑树”,索引靠 hash 扰动和位运算,容量始终 2 的幂。put 命中桶,若树用树,链表用尾插,链表≥8且表≥64才树化,否则先扩容。扩容搬迁靠位运算判分组,不重新 hash,并发下不安全,推荐用 ConcurrentHashMap。
ConcurrentHashMap 1.8 深度笔记与速答
一、诞生背景
- HashMap:线程不安全,多线程 put 会丢数据/死循环。
- Hashtable:整表一把锁,性能极差。
ConcurrentHashMap = 线程安全 + 高性能的 HashMap。
二、底层结构
- 数组 + 链表 + 红黑树与 HashMap 1.8 相同。
- 数组类型:
Node<K,V>[] table,桶里冲突少用链表,多时用红黑树。
可简单认为:加了并发控制的 HashMap。
三、三大核心特性
1. get:无锁读取
- hash 定位桶,链表 or 树方式查找。
- 全过程不加锁,只用 volatile 保证可见性,多线程读极快。
2. put:只锁当前桶,小锁极细粒度
-
定位桶,若空用 CAS 插入,无锁。
-
非空则对桶头节点 synchronized,只锁“一小块”。
-
对比:
- Hashtable:锁整表,写任何地方都竞争。
- CHM:写不同桶互不影响。
3. 扩容:多线程协作搬迁
- 多线程拆分桶任务,一起搬数据。
- 被迁移过的桶用
ForwardingNode特殊节点标记,线程可判断并协作或转向新表。
四、JDK 1.7/1.8 差别快答
- **1.7:**分段锁(Segment)+每段锁一把;
- **1.8:**去掉 Segment,直接桶粒度锁,更灵活高效。
五、面试重点与小结
- 不允许
null key/null value。 - get 无锁,多线程读极快。
- put/remove 只锁单个桶,极小锁冲突。
- size() 高并发场景下为近似值。
- 迭代弱一致,不抛 ConcurrentModificationException。
六、面试超浓缩模板
ConcurrentHashMap 1.8:底层和 HashMap 1.8 类似,但做了并发优化。get 无锁,仅读 volatile。put 时如果桶空用 CAS 插入,桶非空只对桶头加 synchronized,不锁全表。扩容支持多线程,协同搬迁桶数据。多线程安全且高性能。