高 困难
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 8 | JDK 11 | JDK 17 |
|---|---|---|---|
| String | char[] | byte[] (compact) | byte[] + 更多API |
| HashMap | 数组+链表+红黑树 | 同 | 同 |
| ConcurrentHashMap | CAS+synchronized | 同 | 同 |
| 接口 | default 方法 | private 方法 | sealed 接口 |
| Switch | 传统 | 同 | 模式匹配(preview) |
| Records | 无 | 无 | 正式支持 |
| var 局部推断 | 无 | 支持 | 支持 |
| GC 默认 | Parallel | G1 | G1 (ZGC成熟) |
| HTTP Client | 无 | java.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