一文搞懂HashMap底层原理

前言

HashMap是Java集合核心,本文从底层结构、哈希算法、读写流程、扩容、红黑树、线程安全、衍生集合全方位拆解。

一、底层数据结构(JDK1.7 vs JDK1.8)

1. JDK1.7

数组 + 单向链表(Entry节点)
冲突元素全部挂链表,采用头插法,并发扩容会产生环形链表死循环。

2. JDK1.8(主流生产版本)

数组(Node[]桶) + 单向链表(Node) + 红黑树(TreeNode)
规则:

  1. 同一桶内链表长度 ≥ 8 且数组容量≥64:链表转为红黑树;
  2. 红黑树节点 ≤ 6:退化为普通链表;
  3. 数组容量不足64时,链表过长优先扩容,不树化。

节点结构

// 普通链表节点
static class Node<K,V> implements Map.Entry<K,V> {
    final int hash;
    final K key;
    V value;
    Node<K,V> next;
}
// 红黑树节点,继承Node,新增左右、父、颜色指针
static final class TreeNode<K,V> extends Node<K,V> {
    TreeNode<K,V> parent;
    TreeNode<K,V> left;
    TreeNode<K,V> right;
    TreeNode<K,V> prev;
    boolean red;
}

核心常量

  • 默认初始容量:16(2的4次幂)
  • 默认负载因子:0.75
  • 树化阈值:TREEIFY_THRESHOLD=8
  • 退化阈值:UNTREEIFY_THRESHOLD=6
  • 最小树化容量:MIN_TREEIFY_CAPACITY=64

二、哈希与扰动函数(解决哈希碰撞)

1. 底层hash方法

static final int hash(Object key) {
    int h;
    // key.hashCode() 高16位 ^ 低16位,混合高低位
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

2. 为什么需要扰动?

数组长度永远是2的幂,hash & (n-1) 只会保留hash低位;
如果hashCode高低位差异大、低位重复,极易大量碰撞。
右移16位异或,把高位特征混入低位,大幅提升低位随机性,减少碰撞。

3. 为什么容量必须是2的整数幂?

计算下标公式:i = hash & (table.length - 1)

  1. 位运算&替代取模%,性能更高;
  2. length=2^n时,length-1二进制全1,hash所有低位都能参与散列,元素均匀分布;
  3. 扩容时仅判断hash & oldCap即可快速拆分节点,无需重算hash。

4. 初始化传入非2次幂怎么处理?

调用tableSizeFor()向上取最近的2次幂。
示例:new HashMap(17),实际容量32。

static final int tableSizeFor(int cap) {
    int n = cap - 1;
    n |= n >>> 1; n |= n >>> 2; n |= n >>> 4;
    n |= n >>> 8; n |= n >>> 16;
    return (n < 0) ? 1 : (n >= MAX) ? MAX : n + 1;
}

逻辑:把数字所有低位全部填充1,最后+1得到2的幂。

三、put插入完整流程

  1. 计算key扰动hash值;
  2. 判断table数组是否为空,为空执行resize()初始化;
  3. 计算桶下标i = hash & (n-1)
  4. 桶为空:直接新建Node放入桶,结束;
  5. 桶不为空:
    • 桶首节点key完全相等(hash+equals):直接覆盖value;
    • 桶是TreeNode红黑树节点:执行树插入;
    • 桶是普通链表:尾插法遍历链表,找到相同key则覆盖;遍历到末尾无匹配则追加新节点;
  6. 链表新增节点后,长度达到8:调用treeifyBin()
    • 数组<64:仅扩容,不树化;
    • 数组≥64:链表转红黑树;
  7. size自增,判断size > threshold,满足则触发扩容resize。

JDK1.7头插法,JDK1.8改为尾插法,避免并发环形链表。

四、get查询流程

  1. 扰动计算hash,定位桶下标;
  2. 桶首节点匹配hash+key.equals,命中直接返回;
  3. 判断节点是TreeNode,遍历红黑树查找;
  4. 否则循环遍历单向链表匹配key;
  5. 未匹配返回null。

五、负载因子0.75与扩容机制

1. 负载因子为什么是0.75?

负载因子=元素数/数组容量,平衡空间开销哈希碰撞概率

  • 过小:提前扩容,占用更多内存;
  • 过大:桶填充密集,碰撞激增,链表/树查询变慢;
    泊松分布计算下,0.75是综合最优平衡点。
    阈值计算公式:threshold = capacity * 0.75
    默认16容量,阈值12,第13个元素插入触发扩容。

2. resize扩容核心优化(JDK1.8重点)

扩容后容量 = 原容量 * 2;
旧节点不需要重新计算hash,仅判断 hash & oldCap

  • 结果=0:新下标 = 原下标;
  • 结果≠0:新下标 = 原下标 + oldCap;
    链表拆分为高低两条链表,分别放入新桶,无需遍历重hash,性能大幅提升。

3. 树节点扩容

红黑树同样按hash & oldCap拆分两条树链;
拆分后单链节点≤6,自动退化为普通链表。

六、红黑树相关面试问题

1. 为什么链表转红黑阈值是8?

哈希随机分布下,桶内节点数量符合泊松分布,链表长度达到8的概率仅千万分之六,属于极端碰撞场景,树化作为兜底方案。
红黑树节点占用内存约为普通Node两倍,正常短链表查询更快,不会提前树化浪费内存。

2. 退化阈值为什么是6不是8?

防止节点数量在7/8反复增减,频繁树化、退化,产生大量转换开销,设置缓冲区间。

3. 为什么不用平衡二叉树(AVL)?

AVL严格平衡,插入删除频繁旋转;红黑树弱平衡,最多2次旋转即可恢复,读写综合性能更高。

4. 红黑树五大规则

  1. 节点只能红/黑;
  2. 根节点黑色;
  3. 所有叶子空节点黑色;
  4. 红色节点子节点必为黑色;
  5. 任意节点到所有叶子,黑色节点数量相等。
    平衡依靠变色、左旋、右旋维护。

七、哈希冲突解决四大方案(拓展)

  1. 链地址法(HashMap采用):冲突元素挂链表/红黑树;
  2. 开放寻址法:线性探测、二次探测;
  3. 再哈希法:冲突换哈希函数重算下标;
  4. 公共溢出区:单独数组存放冲突数据。

八、HashMap线程不安全问题(高频面试)

HashMap完全非线程安全,无任何同步控制:

  1. 数据覆盖丢失:多线程同时put同一桶,后插入节点覆盖前者;
  2. JDK1.7特有:头插法并发扩容,链表倒置形成环形链表,get无限循环CPU100%;
  3. JDK1.8尾插消除死链,但仍存在size计数错乱、红黑树结构破坏、扩容数据丢失;

并发替代方案对比

  1. HashTable:全方法synchronized锁整张表,并发性能极差;
  2. Collections.synchronizedMap:全局对象锁,等同于HashTable;
  3. ConcurrentHashMap(推荐):细粒度锁。

九、ConcurrentHashMap实现原理

JDK1.7:分段锁Segment

  • 底层:Segment数组+链表,Segment继承ReentrantLock;
  • 默认16个分段,最多16线程并发写,仅锁住当前Segment;
  • get无锁,value用volatile保证可见性。

JDK1.8:CAS + synchronized(锁单个桶头节点)

结构和HashMap一致(数组+链表+红黑树):

  1. 桶为空:CAS无锁插入;
  2. 当前桶正在扩容:协助迁移数据;
  3. 桶有元素:synchronized(f)锁住桶首节点,仅锁单个桶,粒度极细;
  4. 不允许key/value为null,防止并发歧义;
  5. get全程无锁,依靠volatile数组、节点可见性读取。

十、衍生集合底层原理

1. LinkedHashMap

继承HashMap,额外维护双向链表(before/after),实现有序:

  • 插入有序(默认);
  • 访问有序(LRU缓存核心,重写removeEldestEntry淘汰旧数据)。

2. TreeMap

底层纯红黑树,按键排序;key需实现Comparable或传入自定义Comparator。

3. HashSet

底层封装HashMap,存入元素作为key,value统一固定Object常量PRESENT;add、remove、contains全复用HashMap方法。

十一、高频面试简答汇总

  1. 初始化new HashMap(10)实际容量?16,tableSizeFor向上取2次幂;
  2. 允许null键值?HashMap允许1个null key,ConcurrentHashMap不允许;
  3. 链表查询时间复杂度O(n),红黑树O(logn);
  4. 扩容时机:size > capacity * 0.75;
  5. 1.7与1.8核心差异:红黑树、尾插、扩容优化、扰动简化;
  6. 热key大量碰撞解决方案:自定义哈希、扩容、分段存储。
posted @ 2026-07-22 17:08  七星6609  阅读(30)  评论(0)    收藏  举报