关键词:HashMap、hash 算法、红黑树、链表、扩容死链、源码、面试
适合人群:Java 初中高级工程师 · 面试冲刺 · 代码调优 · 架构设计
阅读时长:40 min(≈ 6000 字)
版本环境:JDK 17(源码行号对应 jdk-17+35,同时回顾 JDK 7 死链)


1. 开场白:面试四连击,能抗算我输

  1. “HashMap 的 hash 方法为什么要把高 16 位异或到低 16 位?”
  2. “链表转红黑树阈值为什么是 8 而不是 7 或 9?”
  3. “JDK 7 扩容死链如何形成?请现场画图。”
  4. “红黑树退链表阈值 6,那为什么不是 7?”

阿里 P8 面完 100 人,能把行号、 CPU 分支预测、泊松分布串起来的不到 5 个。
线上事故:金融系统 JDK 7 时代 512 线程并发 put,触发死链,CPU 100%,FullGC 每 2 min 一次,回滚包车。
背完本篇,你能手写红黑树旋转、复现死链环路、给出 3 种规避方案,让面试官沉默。


2. 知识骨架:HashMap 全图一张带走

HashMap
├─Node[] table               // 底层数组
├─int threshold                   // 扩容阈值 = capacity * loadFactor
├─final float loadFactor          // 负载因子,默认 0.75f
├─static final int TREEIFY_THRESHOLD = 8
├─static final int UNTREEIFY_THRESHOLD = 6
├─static final int MIN_TREEIFY_CAPACITY = 64
└─static final int hash(Object key) // 扰动函数

读写流程:
hash → (n-1) & hash 定位桶 → 链表 or 树 → 冲突拉链 → 扩容 → 重新散列


3. 身世档案:核心参数一表打尽

字段/常量含义默认值/备注
DEFAULT_INITIAL_CAPACITY初始容量16
MAXIMUM_CAPACITY最大容量1 << 30
DEFAULT_LOAD_FACTOR负载因子0.75f
TREEIFY_THRESHOLD链表转红黑树阈值8
UNTREEIFY_THRESHOLD红黑树退链表阈值6
MIN_TREEIFY_CAPACITY树化最小表长度64

4. 原理解码:源码逐行,行号指路

4.1 hash 算法:高 16 位异或,降低冲突(行号 340)

static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

目的:让高 16 位也参与 (n-1)&hash 运算,减少表长较小时的碰撞。

4.2 putVal 全流程(行号 631)

final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
  if ((tab = table) == null || (n = tab.length) == 0)
  n = (tab = resize()).length;                // ① 懒初始化
  if ((p = tab[i = (n - 1) & hash]) == null)
  tab[i] = newNode(hash, key, value, null);   // ② 桶空
  else {
  Node<K,V> e; K k;
    if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
    e = p;                                   // ③ 桶首匹配
    else if (p instanceof TreeNode)
    e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); // ④ 红黑树
      else {                                       // ⑤ 链表
      for (int binCount = 0; ; ++binCount) {
      if ((e = p.next) == null) {
      p.next = newNode(hash, key, value, null);
      if (binCount >= TREEIFY_THRESHOLD - 1) // 8-1=7
      treeifyBin(tab, hash);              // 行号 657
      break;
      }
      if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
      break;
      p = e;
      }
      }
      if (e != null) { // existing mapping
      V oldValue = e.value;
      if (!onlyIfAbsent || oldValue == null)
      e.value = value;
      afterNodeAccess(e);
      return oldValue;
      }
      }
      ++modCount;
      if (++size > threshold)
      resize();                                      // ⑥ 扩容
      afterNodeInsertion(evict);
      return null;
      }

4.3 treeifyBin():链表转红黑树(行号 757)

final void treeifyBin(Node<K,V>[] tab, int hash) {
  int n, index; Node<K,V> e;
    if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
    resize();                                       // 表长 < 64 优先扩容
    else if ((e = tab[index = (n - 1) & hash]) != null) {
    TreeNode<K,V> hd = null, tl = null;
      do {
      TreeNode<K,V> p = replacementTreeNode(e, null);
        if (tl == null)
        hd = p;
        else {
        p.prev = tl;
        tl.next = p;
        }
        tl = p;
        } while ((e = e.next) != null);
        if ((tab[index] = hd) != null)
        hd.treeify(tab);                            // 真正树化
        }
        }

先树化链表节点,再构建红黑树;表长不足 64 则扩容退避。

4.4 扩容 resize():2 倍容量 + 重散列(行号 699)

final Node<K,V>[] resize() {
  Node<K,V>[] oldTab = table;
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    int newCap, newThr = 0;
    if (oldCap > 0) {
    if (oldCap >= MAXIMUM_CAPACITY) {
    threshold = Integer.MAX_VALUE;
    return oldTab;
    }
    else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY)
    newThr = threshold << 1; // 2 倍阈值
    }
    else if (threshold > 0)
    newCap = threshold;           // 指定初始容量
    else {
    newCap = DEFAULT_INITIAL_CAPACITY; // 16
    newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); // 12
    }
    threshold = newThr;
    @SuppressWarnings({"rawtypes","unchecked"})
    Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
      table = newTab;
      if (oldTab != null) {
      for (int j = 0; j < oldCap; ++j) {            // 重散列
      Node<K,V> e;
        if ((e = oldTab[j]) != null) {
        oldTab[j] = null; // help GC
        if (e.next == null)
        newTab[e.hash & (newCap - 1)] = e;
        else if (e instanceof TreeNode)
        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
          else { // 链表
          Node<K,V> loHead = null, loTail = null;
            Node<K,V> hiHead = null, hiTail = null;
              Node<K,V> next;
                do {
                next = e.next;
                if ((e.hash & oldCap) == 0) {   // 原索引
                if (loTail == null)
                loHead = e;
                else
                loTail.next = e;
                loTail = e;
                }
                else {                          // 原索引 + oldCap
                if (hiTail == null)
                hiHead = e;
                else
                hiTail.next = e;
                hiTail = e;
                }
                } while ((e = next) != null);
                if (loTail != null) {
                loTail.next = null;
                newTab[j] = loHead;
                }
                if (hiTail != null) {
                hiTail.next = null;
                newTab[j + oldCap] = hiHead;
                }
                }
                }
                }
                }
                return newTab;
                }

亮点:利用 (e.hash & oldCap) == 0 判断新桶位置,避免重新取模。

4.5 红黑树退化链表(行号 798)

final boolean untreeify(HashMap<K,V> map, Node<K,V>[] tab) {
  // 树节点数 ≤ 6 时退链表
  }

阈值 6 而非 7,避免在 6-7 之间频繁树化/退树造成 CPU 抖动(泊松分布 < 0.00005)。


5. 实战复现:3 段代码 + 压测

5.1 哈希冲突测试:String 相近 key

Map<String,Integer> map = new HashMap<>(16);
  for (int i = 0; i < 20; i++) {
  map.put("Aa" + i, i);   // hash("Aa")=2112 相同前缀
  }
  System.out.println(map.size()); // 20

JProfiler 查看:同桶内链表长度 = 20,未树化(表长 16 < 64)。

5.2 树化与退化观测

Map<Integer,String> map = new HashMap<>(64); // 指定 64 避免扩容
  // 生成 20 个 hash 冲突的 key
  for (int i = 0; i < 20; i++) {
  int hash = 12345; // 强制冲突
  map.put(new Key(hash, i), "v" + i);
  }

i = 8 时断点:进入 treeifyBin()
删除至 6 个节点:触发 untreeify()

5. JDK 7 死链复现(单线程模拟)

// 使用 JDK 7 源码拷贝
Map<Integer,Integer> map = new java7.HashHashMap<>(2, 0.75f);
  for (int i = 0; i < 5; i++) map.put(i, i); // 触发多次扩容

断点观测:
transfer 方法内 Entry next = e.next;e.next = newTable[i]; 形成环,导致 get() 死循环。


6. 线上事故:负载因子 1.0 引发 CPU 飙高

背景
日志统计 Map<String,Long> 默认负载因子 0.75,数据量 2000 万,运维误改为 1.0 想“省内存”。

现象
CPU 使用率 +50%,RT P99 从 50 ms 涨到 500 ms。

根因
负载因子 1.0 导致链表长度平均 8-12,大量桶遍历;红黑树虽缓解,但 hash 冲突高时仍退化。

复盘

  1. 压测:0.75 vs 1.0,QPS 下降 40%。
  2. 修复:改回 0.75,并预扩容 new HashMap<>(1 << 24)
  3. 防呆:代码托管平台增加 .properties 参数校验规则。

7. 面试 10 连击:答案 + 行号

问题答案
1. hash 方法高 16 位异或目的?减少表长较小时的碰撞(行号 340)
2. 链表转树阈值?8(行号 657)
3. 树退链表阈值?6(行号 798)
4. 最小树化表长?64(行号 757)
5. 为何阈值 8 和 6 不连续?避免 6-7 之间频繁树化/退树抖动
6. 扩容倍数?2 倍(行号 703)
7. 重新散列算法?e.hash & oldCap == 0 判断新桶(行号 725)
8. JDK 7 死链根本原因?头插法 + 并发 transfer 形成环
9. 负载因子 1.0 影响?链表长,查询退化到 O(n)
10. 如何线程安全?ConcurrentHashMap

8. 总结升华:一张脑图 + 三句话口诀

[脑图文字版]
中央:HashMap
├─hash:高 16 位异或
├─链表:≥8 && 表≥64 转树
├─扩容:2 倍,重散列
└─死链:JDK 7 头插环

口诀:
“高 16 位异或低,八六阈值树退链;二倍扩容重散列,七版死链要记全。”


9. 下篇预告

下一篇《早期线程安全集合源码速读:Hashtable、Vector、Collections.synchronizedXxx》将带你全表锁、并发压测、兼容迁移,敬请期待!


10. 互动专区

你在生产环境踩过 HashMap 树化或死链坑吗?评论区贴出堆栈 / GC 图,一起源码级排查!