面试知识库
困难

Java基础源码追问清单#

一句话答案#

Java 基础源码追问集中在 HashMap(resize 死循环/红黑树阈值/扰动函数)、ConcurrentHashMap(CAS+synchronized 协同/size 统计)、String(不可变性/intern 池/拼接优化)三大类,JDK 8→11→17 的关键差异也是高频追问。

核心要点

一、HashMap 源码追问

追问答案要点
扰动函数为什么 hash^(hash>>>16)?高16位参与运算,减少低位碰撞;长度为2^n时 (n-1)&hash 只用低位,扰动让高位也影响桶定位
为什么容量必须是 2^n?(capacity-1)&hash 等价于 hash%capacity 但位运算更快;resize 时元素要么在原位要么在 原位+oldCap
链表→红黑树阈值为什么是8?泊松分布下链表长度达8的概率约 0.00000006,基本不会触发;红黑树查找O(logn)但维护成本高
红黑树→链表阈值为什么是6(不是8)?避免频繁在7-8之间切换(抖动),6和8之间留了buffer
JDK 7 resize 为什么死循环?头插法+多线程→环形链表;JDK 8 改尾插法+高低位拆分解决
resize 时元素怎么分布?(e.hash & oldCap) == 0 留原位,否则迁移到 原位+oldCap;不需要重新计算桶位
put 时 key 为 null?专门放在 table[0],hash 值固定为 0

二、ConcurrentHashMap 源码追问(JDK 8)

追问答案要点
为什么放弃分段锁?JDK 7 Segment 固定16段,并发度受限且内存浪费;JDK 8 改为对每个桶头节点加 synchronized
CAS 和 synchronized 怎么协同?桶为空时 CAS 写入(无锁);桶非空时 synchronized 锁头节点(细粒度锁)
size() 怎么统计的?baseCount + CounterCell[](LongAdder 思想),CAS 更新 baseCount 冲突时分散到 cell
扩容时其他线程能读写吗?能。ForwardingNode 标记已迁移桶,读请求转发到新表;其他线程还能帮助迁移(helpTransfer)
key/value 能为 null 吗?都不能。因为无法区分”key不存在返回null”和”value本身是null”,在并发场景有歧义
transfer 迁移怎么做?stride 步长,每个线程负责一段桶的迁移,CAS 竞争分配任务

三、String 源码追问

追问答案要点
为什么不可变?final class + final char[](JDK9 改 byte[]) + 无修改方法;安全性(HashMap key/线程安全) + 缓存hashCode
intern() 机制?检查常量池(StringTable)是否有相同字符串,有则返回引用,无则加入。JDK 7+ StringTable 在堆中
”a”+“b” 编译优化?编译期直接合并为 “ab”;变量拼接 JDK 8 用 StringBuilder,JDK 9+ 用 invokedynamic+StringConcatFactory
JDK 9 Compact Strings?char[]→byte[] + coder 标记(LATIN1/UTF16),纯ASCII字符串内存减半

四、JDK 版本关键差异

特性JDK 8JDK 11JDK 17
Stringchar[]byte[] (compact)byte[] + 更多API
HashMap数组+链表+红黑树
ConcurrentHashMapCAS+synchronized
接口default 方法private 方法sealed 接口
Switch传统模式匹配(preview)
Records正式支持
var 局部推断支持支持
GC 默认ParallelG1G1 (ZGC成熟)
HTTP Clientjava.net.http
模块系统支持(JPMS)支持
面试回答(2分钟版)

Java 基础源码我重点准备了三块。HashMap 方面,扰动函数 hash 异或高16位是为了让高位参与桶定位减少碰撞,容量必须是 2^n 保证位运算取模,链表转红黑树阈值8是因为泊松分布下概率极低,JDK 7 的 resize 死循环是因为头插法在多线程下形成环,JDK 8 改了尾插法并用 hash&oldCap 判断元素迁移位置。ConcurrentHashMap 方面,JDK 8 放弃了固定16段的分段锁改为每个桶头节点 synchronized,空桶 CAS 写入非空桶加锁,size 用 baseCount+CounterCell 分散统计避免竞争,扩容时 ForwardingNode 转发读请求并支持多线程协助迁移。String 方面,不可变保证了 HashMap key 安全和 hashCode 缓存,JDK 9 Compact Strings 把 char 数组改成 byte 数组 ASCII 字符串内存减半。JDK 版本差异我会重点提 var 类型推断、Records、sealed classes 和 GC 从 Parallel 到 G1 再到 ZGC 的演进。

追问与易错

追问方向:

  • “HashMap 线程不安全的具体表现?”→ JDK7 并发扩容头插法形成环形链表导致死循环;JDK8 并发 put 数据覆盖丢失、size 计数不准确
  • “ConcurrentHashMap 的 weakly consistent 迭代器?”→ 迭代器创建时基于当前快照,之后其他线程的修改可能可见也可能不可见,不会抛 ConcurrentModificationException
  • “String.hashCode() 为什么用 31?”→ 31 是奇质数,碰撞率低分布均匀;且编译器可将 31 * i 优化为 (i << 5) - i 位运算,兼顾散列质量和计算性能
  • “equals 和 hashCode 必须同时重写?”→ 是的,HashMap 先用 hashCode 定位桶再用 equals 逐个比较,若 equals 相等但 hashCode 不同会定位到错误的桶,导致 get 返回 null

易错点:

  • ❌ “JDK 8 HashMap 线程安全了”——只是修复了死循环,仍然线程不安全
  • ❌ “ConcurrentHashMap 的 size 是精确的”——是近似值,并发修改中可能不精确
  • ❌ “String + 拼接很慢”——编译器优化后单次拼接和 StringBuilder 一样,循环拼接才需要手动用 StringBuilder
  • ✅ 记住关键数字:红黑树阈值8/6、扰动右移16位、默认负载因子0.75、初始容量16