Java语言基础
面试权重:★★★ | 适用级别:P7/P8+ | 预计复习时间:2-3周
概览
面试权重:★★★ | 适用级别:P7/P8+ | 预计复习时间:2-3周
这一章是所有后续专题的底座。真正难的不是“背过多少点”,而是能否把语言机制、集合、并发和 JUC 串成一条完整的面试回答链路。
建议用法
先用正文建立理解,再用后面的“高频面试题”做抽背和口述训练。如果某个题讲不顺,优先回看对应正文块,而不是继续堆新题。
一、知识体系
1. Java基础
1.1 面向对象 ★★
封装、继承、多态
- 核心结论
- 封装的目标是隐藏实现细节、暴露稳定行为边界,本质是控制对象状态的可见性和修改方式。
- 继承表达的是
is-a关系,适合复用稳定抽象;如果只是为了偷懒复用代码,通常更应该考虑组合。 - 多态的本质不是一句“父类引用指向子类对象”,而是“编译期看声明类型,运行期走实际对象的方法实现”。
- 原理展开
- Java 方法调用分静态绑定和动态绑定。
static、private、final方法不会被重写,调用目标在编译期就能确定。 - 普通实例方法依赖运行时分派,JVM 会基于实际对象类型找到最终实现。
- 字段没有多态,字段访问取决于引用的编译期类型;发生动态分派的是可重写实例方法。
- Java 方法调用分静态绑定和动态绑定。
- 面试怎么答
- “封装解决边界和约束,继承解决稳定抽象复用,多态解决扩展替换。Java 里真正动态分派的是普通实例方法,不是字段,也不是静态方法。”
- 易错点
- 继承不是默认复用方式,很多业务代码实际上更适合组合。
- 子类不能重写父类的
private方法,只是定义了同名新方法。
抽象类 vs 接口
- 核心结论
- 抽象类更适合表达“一类对象的公共状态和默认行为”。
- 接口更适合表达“能力约束”或“跨层契约”。
- Java 8 之后接口有
default方法,Java 9 之后支持私有方法,但接口依然不应该承担复杂状态管理。
- 关键对比
| 维度 | 抽象类 | 接口 |
|---|---|---|
| 设计目的 | 表达一类对象的共性 | 表达一种能力或契约 |
| 状态 | 可定义实例字段 | 只能有常量 |
| 构造器 | 有 | 没有 |
| 继承关系 | 单继承 | 可多实现 |
| 默认实现 | 可以 | Java 8+ 支持 default |
| 典型场景 | 模板方法、框架骨架类 | SPI、服务契约、回调能力 |
- 面试怎么答
- “如果需要抽出公共状态和部分实现,我会选抽象类;如果是定义能力边界、希望解耦实现与调用方,我会选接口。接口可以有默认实现,但它的主要职责仍是契约。”
- 易错点
- 不要因为接口能写
default方法,就把大量业务逻辑堆在接口里。
- 不要因为接口能写
重载 vs 重写
- 核心结论
- 重载发生在同一作用域内,方法名相同、参数列表不同,属于编译期多态。
- 重写发生在父子类之间,运行期基于实际对象进行动态分派。
- 关键规则
- 重载不能只靠返回值区分。
- 重写时访问权限不能更严格,抛出的受检异常不能比父类更宽。
static方法是隐藏不是重写。
- 面试怎么答
- “重载看参数列表,编译期确定;重写看实际对象类型,运行期确定。”
- 易错点
- 很多人把同名
static方法当成重写,这是错误的。
- 很多人把同名
深拷贝 vs 浅拷贝,Cloneable 的陷阱
- 核心结论
- 浅拷贝只复制对象本身,引用字段仍指向原对象。
- 深拷贝会递归复制对象图,拷贝后的可变对象互不影响。
- 原理展开
Object.clone()默认是字段级浅拷贝。Cloneable只是一个标记接口,不实现就会抛CloneNotSupportedException。- 真正需要深拷贝时,更常用拷贝构造函数、工厂方法或显式映射,而不是依赖
clone()。
- 面试怎么答
- “浅拷贝复制的是壳,深拷贝复制的是对象图。
clone()默认并不是深拷贝,而且 API 设计并不优雅,生产里我更倾向显式拷贝。”
- “浅拷贝复制的是壳,深拷贝复制的是对象图。
- 易错点
- 对不可变对象没必要过度深拷贝,例如
String、包装类等。
- 对不可变对象没必要过度深拷贝,例如
1.2 核心机制 ★★★
== vs equals() vs hashCode()
- 核心结论
==比较的是基本类型的值,或引用类型是否指向同一对象。equals()用来表达“逻辑相等”,默认实现和==一样,但很多类会重写。hashCode()不是拿来判断相等的,而是为哈希容器提供更快的定位能力。
- 原理展开
equals()和hashCode()的契约是:如果两个对象equals()为true,则hashCode()必须相等;反过来不要求成立。HashMap/HashSet先用hashCode()定桶,再用equals()做最终判等,因此“只重写equals()不重写hashCode()”会直接破坏集合语义。- 常见实现需要满足自反性、对称性、传递性、一致性,以及对
null返回false。
- 面试怎么答
- “
==看的是值或地址,equals()看的是逻辑相等,hashCode()是哈希结构的定位依据。真正高频的是equals()/hashCode()契约,以及它对HashMap和HashSet行为的影响。”
- “
- 常见追问
- 为什么重写
equals()必须同时重写hashCode()? hashCode()相同是否一定相等?- 自定义类作为
HashMapkey 有什么要求?
- 为什么重写
- 易错点
- 可变对象不适合作为哈希 key。只要参与
equals()/hashCode()的字段发生变化,后续查找就可能异常。
- 可变对象不适合作为哈希 key。只要参与
高频易错点
把可变业务对象直接作为 HashMap key,是线上常见坑。对象放入 map 后如果参与哈希计算的字段变了,后续就可能再也取不出来。
String 不可变性、String Pool、intern()
- 核心结论
String不可变的本质是:内部字符存储不对外暴露修改入口,值一旦确定就不能变。- 不可变带来的收益包括线程安全、可缓存哈希值、可作为 map key、可支持字符串常量池复用。
- 字符串常量池不仅节省内存,也支撑了编译期常量折叠和运行时复用。
- 原理展开
- 现代 JDK 中
String底层是byte[] + coder,但“不可变”的抽象语义没有变。 - 字面量
"abc"在类加载阶段会进入字符串常量池;new String("abc")会在堆上额外创建对象。 intern()的作用是返回常量池中的规范化引用。JDK 7+ 开始常量池放在堆里,intern()更接近“复用已有对象引用”,而不是早期版本那样复制一份。
- 现代 JDK 中
- 面试怎么答
- “String 不可变并不只是
final修饰,而是设计上不提供状态修改能力。它因此天然线程安全、适合做 key,也让字符串常量池成立。intern()的本质是做字符串规范化引用,JDK 7+ 和 JDK 6 之前要分开说。”
- “String 不可变并不只是
- 常见追问
- 为什么 String 要设计成不可变?
new String("a")创建了几个对象?- JDK 6 和 JDK 7+ 的
intern()差异是什么?
- 易错点
- String 不可变不等于“永远不分配新对象”,拼接、格式化、截取都可能产生新对象或中间对象。
String / StringBuilder / StringBuffer
- 核心结论
- 单线程频繁拼接优先
StringBuilder。 - 多线程共享可变字符串缓冲区才考虑
StringBuffer。 - 少量字面量拼接可以直接用
+,编译器通常会优化为StringBuilder。
- 单线程频繁拼接优先
- 对比表
| 类型 | 是否可变 | 线程安全 | 典型场景 |
|---|---|---|---|
String | 否 | 是 | 不变文本、常量、key |
StringBuilder | 是 | 否 | 单线程高频拼接 |
StringBuffer | 是 | 是 | 老代码或多线程共享缓冲区 |
- 面试怎么答
- “三者核心区别就是可变性和线程安全。性能上一般是
StringBuilder最好,String适合不可变值对象,StringBuffer在新项目里用得少。”
- “三者核心区别就是可变性和线程安全。性能上一般是
- 易错点
- 循环中直接做
str = str + x容易产生大量临时对象。
- 循环中直接做
自动装箱/拆箱,Integer 缓存池
- 核心结论
- 装箱本质上调用
valueOf(),拆箱本质上调用xxxValue()。 Integer默认缓存-128 ~ 127,因此这个区间内的装箱对象可能复用。
- 装箱本质上调用
- 原理展开
Integer a = 127; Integer b = 127;常常a == b为true,因为命中了缓存。Integer a = 128; Integer b = 128;常常a == b为false,因为超过缓存范围。- 自动拆箱时如果对象为
null,会直接触发NullPointerException,这是实际项目里的高频坑。
- 面试怎么答
- “自动装箱不是魔法,它只是编译器帮你插入了
valueOf()和intValue()。真正面试高频的是 Integer 缓存和null拆箱 NPE。”
- “自动装箱不是魔法,它只是编译器帮你插入了
- 易错点
- 比较包装类值优先用
equals(),不要依赖==。
- 比较包装类值优先用
反射机制
- 核心结论
- 反射让程序在运行期检查类型、创建对象、访问字段、调用方法,是大多数 Java 框架的基础设施。
- 反射的价值是动态性,代价是类型安全更弱、性能较直接调用差、可读性较低。
- 原理展开
- JVM 加载类后会维护类元数据,Java 程序通过
Class对象访问这些信息。 - 常见入口包括
Class.forName()、obj.getClass()、SomeClass.class。 - 反射调用慢,主要慢在权限检查、方法查找、参数装箱拆箱,以及难以像静态调用那样充分内联优化。
- Spring、MyBatis、Jackson 等框架会混合使用反射、动态代理、字节码增强,把灵活性和性能做平衡。
- JVM 加载类后会维护类元数据,Java 程序通过
- 面试怎么答
- “反射本质是运行期操作类元数据。它让框架具备高度扩展能力,比如依赖注入、注解解析、ORM 映射,但高频路径通常要配合缓存、
MethodHandle或字节码增强。”
- “反射本质是运行期操作类元数据。它让框架具备高度扩展能力,比如依赖注入、注解解析、ORM 映射,但高频路径通常要配合缓存、
- 常见追问
- 反射为什么慢?
- 反射和动态代理是什么关系?
- 框架为什么离不开反射?
- 易错点
- 反射是工具,不是业务代码的默认手段;进入高频调用链后必须考虑缓存和替代方案。
注解(Annotation)
- 核心结论
- 注解本身不提供业务能力,它只是元数据。
- 真正生效的是“谁在什么时机解析了它”,例如编译器、框架、AOP、注解处理器。
- 原理展开
- 元注解最常见的是
@Target、@Retention、@Documented、@Inherited。 RetentionPolicy.SOURCE只在源码阶段有效,CLASS保留到字节码,RUNTIME才能在运行期通过反射读取。- 运行期注解常和代理/拦截器结合,例如
@Transactional、@Cacheable;编译期注解常和 Lombok、MapStruct 这类工具结合。
- 元注解最常见的是
- 面试怎么答
- “注解只是描述信息,关键要讲清楚生效链路:定义注解、指定保留策略,再由编译器或框架去解析执行。很多人只会写
@interface,但说不清运行时是谁让它生效。”
- “注解只是描述信息,关键要讲清楚生效链路:定义注解、指定保留策略,再由编译器或框架去解析执行。很多人只会写
- 常见追问
- 自定义注解要怎么写?
- 为什么
@Retention(RUNTIME)很重要? - Spring 是怎么处理注解的?
- 易错点
@Inherited只对类级注解和继承关系生效,对接口和方法不是同样逻辑。
泛型:类型擦除、通配符与 PECS
- 核心结论
- Java 泛型是编译期类型系统,运行期大部分类型参数会被擦除。
- 泛型的价值是把类型错误尽量前移到编译期,而不是运行期
ClassCastException。 ? extends T适合读,? super T适合写,这就是 PECS:Producer Extends,Consumer Super。
- 原理展开
- 类型擦除后,
List<String>和List<Integer>运行时通常都是List,编译器通过桥接方法、强制类型转换、签名校验来维持类型一致性。 - 不能直接
new T()、不能创建new List<String>[10],本质都和擦除有关。 - 运行期如果要保留泛型信息,往往通过父类/接口签名传递,例如
TypeReference<List<User>>。
- 类型擦除后,
- 面试怎么答
- “Java 泛型不是 reified generics,而是类型擦除模型。它主要在编译期做约束,运行期只保留有限元信息。最容易被问的是通配符边界、PECS 原则,以及框架如何曲线保留泛型信息。”
- 常见追问
- 为什么 Java 要做类型擦除?
List<Object>和List<?>有什么区别?- 如何在运行时获取泛型参数?
- 易错点
? extends T不是“都能加 T”,因为实际子类型可能更窄。- 反射能绕过泛型检查,但只会把问题从编译期拖到运行期。
Java 中的四种引用
- 核心结论
- 强引用:只要还有强引用,GC 就不会回收。
- 软引用:内存不足时才可能回收,适合缓存。
- 弱引用:下一次 GC 就可能回收。
- 虚引用:不影响对象生命周期,只用于跟踪对象回收通知。
- 面试怎么答
- “四种引用的差别不在语法,而在对象的可回收性。最常落地的是缓存用软引用、
ThreadLocalMapkey 用弱引用、虚引用配合ReferenceQueue做资源回收跟踪。”
- “四种引用的差别不在语法,而在对象的可回收性。最常落地的是缓存用软引用、
- 易错点
- 软引用不是稳定缓存方案,内存紧张时会被迅速清掉。
枚举与单例安全性
- 核心结论
- 枚举单例是最稳妥的单例实现之一,因为 JVM 天然保证枚举实例唯一。
- 原理展开
- 枚举实例在类加载阶段创建。
- 枚举天生防反射破坏,也天然兼容序列化语义,不需要额外写
readResolve()。
- 面试怎么答
- “如果只讨论单例安全性,枚举往往是最稳的实现,因为它同时解决了并发、反射和反序列化破坏的问题。”
- 易错点
- 枚举单例稳,不代表它适合所有场景;如果初始化成本高或依赖复杂,也要考虑装载时机。
面试回答提醒
讲泛型不要只背“类型擦除”四个字,最好顺着“编译期检查 -> 运行期擦除 -> 通配符边界 -> 框架如何保留类型信息”这条线回答,会更完整。
1.3 异常体系 ★★
Error vs Exception vs RuntimeException
- 核心结论
Error表示系统级严重问题,通常不要求业务代码恢复。Exception表示程序可以感知和处理的异常情况。RuntimeException是运行时异常,通常由编程错误或违反前置条件导致。
- 原理展开
OutOfMemoryError、StackOverflowError属于Error,重点是排查和治理,不是简单catch。- 受检异常(checked)要求调用方显式处理;非受检异常(unchecked)把处理权留给上层统一机制。
- 面试怎么答
- “三者差异不只是继承层次,而是处理哲学不同:
Error通常不可恢复,checked 异常强调强制处理,unchecked 更适合编程错误和统一异常治理。”
- “三者差异不只是继承层次,而是处理哲学不同:
Checked vs Unchecked Exception
- 核心结论
- checked 异常适合调用方真正能恢复的场景。
- unchecked 异常更适合参数错误、状态非法、系统内部 bug 或统一异常框架处理的场景。
- 对比表
| 维度 | Checked Exception | Unchecked Exception |
|---|---|---|
| 编译期 | 必须显式处理 | 可不处理 |
| 典型场景 | IO、网络、外部资源失败 | 空指针、越界、非法状态 |
| 设计意图 | 强制调用方感知 | 不把业务代码写成 try-catch 森林 |
- 面试怎么答
- “不是 checked 一定更严谨,也不是 unchecked 一定更高级,关键看调用方是否真的有可恢复动作。很多业务项目统一用 unchecked,再配合全局异常处理器,也是一种常见实践。”
- 易错点
- 不要把所有异常都包装成
RuntimeException,把上下文信息丢掉。
- 不要把所有异常都包装成
try-with-resources
- 核心结论
- 它本质是编译器语法糖,最终仍会展开成
try/finally关闭资源。 - 如果
try块和close()都抛异常,try块异常是主异常,关闭异常会作为 suppressed exception 附着。
- 它本质是编译器语法糖,最终仍会展开成
- 原理展开
- 资源类必须实现
AutoCloseable或Closeable。 - 这个语法最大的价值不是“少写 finally”,而是统一正确处理资源关闭和异常压制逻辑。
- 资源类必须实现
- 面试怎么答
- “try-with-resources 的底层仍是 finally 关闭资源,只是编译器帮你生成了模板代码,而且还能保留 suppressed exception,避免手写时把异常链搞丢。”
- 易错点
- 自定义资源类实现
AutoCloseable时,也要设计好close()的异常策略。
- 自定义资源类实现
自定义异常最佳实践
- 核心结论
- 自定义异常应该服务于语义表达、错误分类和统一处理,而不是到处
throw new Exception()。
- 自定义异常应该服务于语义表达、错误分类和统一处理,而不是到处
- 最佳实践
- 按业务域分层定义异常,例如参数异常、状态异常、权限异常、外部依赖异常。
- 保留错误码、用户可见信息、日志上下文,不要只保留一句模糊 message。
- 在系统边界层统一转换,例如 Controller 层映射为 HTTP 状态码和响应体。
- 面试怎么答
- “我会把异常当成领域语义的一部分来设计,让调用方和监控系统能快速判断这是什么问题、该怎么处理。”
2. 集合框架 ★★★
2.1 List
ArrayList
- 核心结论
ArrayList基于动态数组,是绝大多数顺序容器场景的默认选择。- 它的核心优势不只是复杂度公式,而是连续内存带来的 CPU 缓存友好性。
- 原理展开
- JDK 8 中无参构造初始是空数组,第一次
add才扩为默认容量。 - 扩容通常按 1.5 倍增长,在“减少频繁扩容”和“控制内存浪费”之间做平衡。
RandomAccess是一个标记接口,告诉框架“我擅长随机访问”。fail-fast依赖modCount,本质是尽早发现并发修改,不是线程安全机制,也不能保证绝对检测到。
- JDK 8 中无参构造初始是空数组,第一次
- 面试怎么答
- “ArrayList 本质是动态数组,随机访问快、尾插均摊成本低。面试里除了说 1.5 倍扩容,还要补一句它常常比 LinkedList 更快,是因为连续内存更适合 CPU cache。”
- 易错点
- 频繁头插、中间插入、大批量扩容不预设容量,都会让性能明显变差。
LinkedList
- 核心结论
LinkedList基于双向链表,理论上插删节点成本低,但前提是“你已经拿到了目标节点”。- 如果需要先定位位置再插入/删除,真实成本往往不低。
- 原理展开
- 它每个节点都要存前后指针,内存开销更高,局部性更差。
LinkedList同时实现了List和Deque,在双端队列语义下比在随机访问语义下更合理。
- 面试怎么答
- “LinkedList 理论复杂度不差,但真实项目里因为节点分散、缓存不友好、内存开销高,通常只有在双端插删频繁时才值得选。”
- 易错点
- 不要因为“插入删除 O(1)”就草率选 LinkedList,真实业务里大多数时候都输给 ArrayList。
CopyOnWriteArrayList
- 核心结论
- 这是典型的读优化容器:读无锁、写复制。
- 适合读远多于写、且可以接受短暂弱一致的场景。
- 原理展开
- 写操作先加锁复制底层数组,再在新数组上修改,最后替换引用。
- 读操作直接读取当前数组快照,因此非常快,但可能读到旧数据。
- 面试怎么答
- “CopyOnWriteArrayList 用空间和写放大换读性能,适合监听器列表、黑白名单、配置快照这类读多写少场景,不适合高频写。”
- 易错点
- 大对象数组频繁写入会非常贵。
ArrayList vs LinkedList 真实性能对比
- 核心结论
- 绝大多数业务场景优先
ArrayList。
- 绝大多数业务场景优先
- 对比表
| 维度 | ArrayList | LinkedList |
|---|---|---|
| 底层结构 | 动态数组 | 双向链表 |
| 随机访问 | 快 | 慢 |
| 尾部追加 | 快 | 快 |
| 中间插入 | 需搬移元素 | 找到节点后快 |
| CPU 缓存友好 | 好 | 差 |
| 内存占用 | 更紧凑 | 更高 |
- 面试怎么答
- “默认选 ArrayList;只有双端操作特别多、确实受益于链表结构时才考虑 LinkedList。”
2.2 Map
HashMap ★★★
- 核心结论
HashMap是“数组 + 冲突桶结构”的混合体,JDK 8 后桶里可能是链表,也可能是红黑树。- 面试里最重要的不是背源码,而是把
put流程、扩容机制、负载因子、树化条件串成一条线。
- 原理展开
- hash 过程会先拿
key.hashCode(),再做高位扰动,目的是让高位信息参与低位计算,减少碰撞。 - 桶下标通常通过
(n - 1) & hash计算,因此容量设为 2 的幂能让位运算既快又均匀。 put大致流程:- key 为
null时走固定桶位逻辑。 - table 未初始化则先初始化。
- 定位桶位,无冲突直接放入。
- 有冲突则进入链表/红黑树处理。
- 插入后 size 超阈值则扩容。
- key 为
- JDK 8 扩容时不再对所有节点重新算 hash,而是根据 oldCap 那一位是 0 还是 1,把节点分到原位置或原位置 + oldCap,迁移成本更低。
- 树化阈值是 8,退化阈值是 6,而且只有数组长度至少 64 才会树化;否则优先扩容。
- hash 过程会先拿
- 面试怎么答
- “HashMap 底层是数组 + 链表/红黑树。
put时先扰动 hash 再按(n-1)&hash定位桶位,冲突时链表尾插或树插入,超过阈值扩容。容量是 2 的幂是为了让位运算定位更快更均匀,0.75 的负载因子则是时间和空间的折中。”
- “HashMap 底层是数组 + 链表/红黑树。
- 常见追问
- 为什么容量必须是 2 的幂?
- 为什么负载因子是 0.75?
- 为什么树化阈值是 8?
- JDK 7 为什么并发扩容会死循环?
- 易错点
HashMap允许nullkey 和nullvalue,但只允许一个nullkey。- 它不是线程安全容器,并发写入时问题不只是结果不准,可能直接结构损坏。
- 可变对象作为 key 会导致查找异常。
- 关键细节速记
- 默认负载因子:
0.75 - 树化阈值:
8 - 退化阈值:
6 - 最小树化容量:
64
- 默认负载因子:
HashMap 作答顺序
推荐顺序:数据结构 -> 定位方式 -> put 流程 -> 扩容机制 -> 为什么 2 的幂/0.75/8 -> 线程安全问题。这样回答最稳。
ConcurrentHashMap ★★★
- 核心结论
ConcurrentHashMap的目标是“在高并发下提供更好的吞吐和更细的锁粒度”,而不是简单给HashMap外面套一层大锁。- JDK 7 和 JDK 8 的实现思路差别非常大,这是高频对比题。
- 原理展开
- JDK 7 采用
Segment分段锁,类似“数组里装多个小 HashMap”,并发度受 segment 数量影响。 - JDK 8 去掉
Segment,改为Node[] + CAS + synchronized:- 初始化 table 用 CAS 控制;
- 空桶插入优先 CAS;
- 桶冲突时锁桶头节点;
- 大量冲突时也会树化;
- 扩容时多个线程可以协助迁移。
size()在并发修改时只能做到近似统计,底层依赖baseCount + CounterCell的分段累加思路,避免单点热点。
- JDK 7 采用
- 面试怎么答
- “JDK 7 的 ConcurrentHashMap 是 Segment 分段锁;JDK 8 变成数组 + CAS + synchronized 的桶级并发控制。核心改进是锁粒度更细,并且通过
CounterCell等结构降低统计热点。”
- “JDK 7 的 ConcurrentHashMap 是 Segment 分段锁;JDK 8 变成数组 + CAS + synchronized 的桶级并发控制。核心改进是锁粒度更细,并且通过
- 常见追问
- JDK 8 为什么还用了
synchronized? - 为什么
size()不是绝对精确? - 为什么不直接给 HashMap 外面套一层 ReentrantLock?
- JDK 8 为什么还用了
- 易错点
ConcurrentHashMap不允许nullkey/value,因为并发场景下无法区分“没找到”和“值就是 null”。- 它保证的是单次操作线程安全,不代表“先查再改”这种复合逻辑天然原子。
TreeMap
- 核心结论
TreeMap基于红黑树,天然有序。- 当你需要范围查询、按 key 排序、取最小/最大元素时,它比 HashMap 更合适。
- 原理展开
- 红黑树通过旋转和变色维持近似平衡,使查找、插入、删除复杂度保持在
O(logN)。 - 排序依据可以是 key 的自然顺序,也可以由
Comparator指定。
- 红黑树通过旋转和变色维持近似平衡,使查找、插入、删除复杂度保持在
- 面试怎么答
- “HashMap 适合无序快速查找,TreeMap 适合有序场景和范围操作,代价是复杂度从均摊 O(1) 变成 O(logN)。”
- 易错点
- 自定义
Comparator必须与equals()语义尽量保持一致,否则可能出现逻辑上不同但排序上相同的键覆盖问题。
- 自定义
LinkedHashMap
- 核心结论
LinkedHashMap在 HashMap 基础上增加了双向链表,既保留哈希查询能力,又保留可预测的遍历顺序。
- 原理展开
- 有两种顺序:插入顺序和访问顺序。
- 开启访问顺序后,读取一个元素会把它移动到链表尾部,因此天然适合做 LRU 缓存。
- 面试怎么答
- “LinkedHashMap 的价值不是更快,而是更有序。高频追问是它如何用访问顺序 +
removeEldestEntry()实现简单 LRU。”
- “LinkedHashMap 的价值不是更快,而是更有序。高频追问是它如何用访问顺序 +
- 易错点
- 这只是单机内存级 LRU,实现简单但线程安全和淘汰策略能力有限,生产更常直接用 Caffeine。
HashMap / Hashtable / ConcurrentHashMap 对比
- 核心结论
- 业务开发里几乎不会优先选 Hashtable。
- 对比表
| 维度 | HashMap | Hashtable | ConcurrentHashMap |
|---|---|---|---|
| 线程安全 | 否 | 是 | 是 |
| 锁粒度 | 无 | 整表 | 更细粒度 |
null key/value | 允许 | 不允许 | 不允许 |
| 性能 | 单线程好 | 并发差 | 并发好 |
| 典型场景 | 单线程、本地变量 | 基本不用 | 高并发读写 |
2.3 Set
HashSet / TreeSet / LinkedHashSet
- 核心结论
Set的核心语义是“去重”,不同实现的主要差异在底层结构和遍历顺序。
- 原理展开
HashSet基于HashMap,元素作为 key 存入,value 使用固定占位对象。TreeSet基于TreeMap,元素有序。LinkedHashSet基于LinkedHashMap,去重同时保持插入顺序。
- 面试怎么答
- “Set 不是一种全新结构,它更多是对 Map 的封装。选型时先看是否需要顺序:无序选 HashSet,有序选 TreeSet,既要去重又要保序选 LinkedHashSet。”
- 易错点
TreeSet元素必须可比较,或者显式提供比较器。
2.4 Queue
PriorityQueue
- 核心结论
- 底层是二叉堆,默认小顶堆。
- 适合“随时取最小/最大值”的场景,例如 TopK、定时任务调度、合并多路有序流。
- 原理展开
- 插入和删除堆顶的复杂度通常是
O(logN)。 - 它只保证堆顶元素有序,不保证整体遍历有序。
- 插入和删除堆顶的复杂度通常是
- 面试怎么答
- “PriorityQueue 不是排序好的 list,而是堆。你只能高效拿到堆顶,不能指望它整体遍历有序。”
- 易错点
- 自定义比较器错误会直接破坏堆语义。
ArrayDeque vs LinkedList 作为队列/栈
- 核心结论
- 如果只是要双端队列或栈,
ArrayDeque通常比LinkedList更好。
- 如果只是要双端队列或栈,
- 原理展开
ArrayDeque基于循环数组,局部性更好、对象开销更小。LinkedList作为 Deque 也能用,但额外节点对象较多。
- 面试怎么答
- “除了需要链表特殊语义的极少数场景,队列/栈我一般首选 ArrayDeque。”
- 易错点
ArrayDeque不允许存null。
BlockingQueue 系列(见并发篇)
- 核心结论
- 这是并发编程里的核心生产者-消费者容器,详细内容放到 JUC 章节理解更自然。
- 补充说明
ArrayBlockingQueue有界、数组实现。LinkedBlockingQueue可选有界,吞吐常更高。SynchronousQueue不存元素,只做线程间直接移交。DelayQueue按到期时间出队。
3. 并发编程 ★★★
3.1 线程基础
线程生命周期与状态转换
- 核心结论
- Java 线程状态是 JVM 层语义,不等于操作系统线程状态,但足够指导面试和排障。
- 关键状态
NEW:已创建,未调用start()RUNNABLE:可运行或正在运行BLOCKED:等待进入synchronized监视器WAITING:无限期等待,如wait()、join()、park()TIMED_WAITING:限时等待,如sleep()、wait(timeout)TERMINATED:执行结束
- 面试怎么答
- “不要只背六种状态,更要说清楚 BLOCKED 和 WAITING 的区别:前者在等锁,后者通常是线程主动挂起等待条件。”
Thread vs Runnable vs Callable
- 核心结论
Thread是线程载体,Runnable是无返回值任务,Callable是有返回值且能抛异常的任务。
- 对比表
| 类型 | 返回值 | 可抛异常 | 典型用法 |
|---|---|---|---|
Thread | 无 | 否 | 直接创建线程 |
Runnable | 无 | 否 | 线程池最常用 |
Callable | 有 | 是 | 任务需要结果时 |
- 面试怎么答
- “现代业务代码很少直接
new Thread(),更常把Runnable/Callable交给线程池调度。”
- “现代业务代码很少直接
sleep() vs wait() vs yield() vs join()
- 核心结论
- 这几个 API 很容易混,但关注点不同:睡眠、等待条件、让出时间片、等待线程结束。
- 对比表
| 方法 | 是否释放锁 | 所属类 | 主要作用 |
|---|---|---|---|
sleep() | 否 | Thread | 让当前线程休眠 |
wait() | 是 | Object | 等待条件,被 notify 唤醒 |
yield() | 否 | Thread | 提示调度器让出 CPU |
join() | 否 | Thread | 等待目标线程结束 |
- 面试怎么答
- “最关键的区别是
wait()释放对象锁并依赖监视器机制,sleep()不释放锁。”
- “最关键的区别是
- 易错点
wait()必须在持有对象监视器时调用,否则抛IllegalMonitorStateException。
线程中断机制
- 核心结论
- Java 中断是协作式取消,不是强杀线程。
- 原理展开
interrupt()只是设置中断标记;线程是否退出取决于自己是否检查标记并响应。Thread.interrupted()会读取并清除当前线程中断标记;isInterrupted()只读取不清除。- 阻塞方法如
sleep()、wait()、join()被中断时会抛InterruptedException,同时清除中断状态。
- 面试怎么答
- “中断不是 stop,而是通知。好的并发代码要么向上抛
InterruptedException,要么在 catch 后重新设置中断标记,不能悄悄吞掉。”
- “中断不是 stop,而是通知。好的并发代码要么向上抛
- 易错点
- 吞掉中断异常而不恢复标记,是线程池和框架代码中的典型 bug。
守护线程(Daemon Thread)
- 核心结论
- 所有用户线程结束后,JVM 不会等待守护线程继续运行。
- 典型场景
- GC 线程、监控清理线程、后台巡检线程。
- 面试怎么答
- “守护线程适合辅助性后台任务,不适合承载必须完成的业务逻辑,因为 JVM 退出时它可能被直接终止。”
3.2 synchronized 与锁机制 ★★★
synchronized 原理
- 核心结论
synchronized是 JVM 内置监视器锁,编译后会落成monitorenter/monitorexit字节码,或者在方法标志里带ACC_SYNCHRONIZED。- 它的底层不是“关键字魔法”,而是对象监视器和对象头状态配合完成的。
- 原理展开
- 每个对象都可以和一个 Monitor 关联。线程进入同步块时尝试获取 Monitor,退出时释放。
- 对象头中的 Mark Word 会在不同状态下记录锁相关信息,例如无锁、轻量级锁、重量级锁的标记。
- JVM 会根据竞争情况做锁优化和锁膨胀,而不是一上来就进入重量级互斥。
- 面试怎么答
- “synchronized 本质是 JVM 级 Monitor 机制。进入同步块时获取 Monitor,退出时释放。对象头 Mark Word 用来承载锁状态,竞争升级后会膨胀成重量级监视器。”
- 常见追问
- 同步方法和同步代码块字节码上有什么区别?
- 为什么说
synchronized是可重入的? - JVM 为什么还要做锁优化?
锁升级:偏向锁 -> 轻量级锁 -> 重量级锁
- 核心结论
- 锁不是固定一种形态,JVM 会根据竞争程度做优化。
- 原理展开
- 无竞争时更倾向用低成本状态;
- 轻度竞争时可通过 CAS 和自旋避免线程阻塞;
- 真正竞争激烈时才膨胀为重量级锁,涉及内核态阻塞与唤醒。
- 版本说明
- 偏向锁是经典面试点,但在较新的 JDK 版本里已经逐步弱化。面试时重点理解思想,不必纠结所有版本细枝末节。
- 面试怎么答
- “锁升级的本质是让低竞争场景更便宜,高竞争场景更安全。核心思路是先乐观、后悲观。”
- 易错点
- 锁升级不是应用层主动控制的,而是 JVM 运行期优化行为。
锁消除与锁粗化
- 核心结论
- JVM 不只是帮你加锁,也会在证明安全时帮你去锁或合并锁。
- 原理展开
- 锁消除通常基于逃逸分析:对象没有逃出当前线程,就没必要真加锁。
- 锁粗化则是把一串连续小锁合成一个大锁,减少频繁加解锁开销。
- 面试怎么答
- “这两个优化说明 JVM 看待锁不是字面执行,而是会结合逃逸分析和热点路径做优化。”
ReentrantLock
- 核心结论
ReentrantLock是 API 级可重入锁,功能比synchronized丰富。
- 关键能力
- 公平锁 / 非公平锁
- 可中断获取:
lockInterruptibly() - 可超时获取:
tryLock(timeout) - 多条件变量:
Condition
- 面试怎么答
- “如果只是普通互斥,synchronized 足够;如果要可中断、可超时、公平策略或多个等待队列,我才会选 ReentrantLock。”
- 易错点
- 使用
lock()后必须try/finally释放,否则容易死锁。
- 使用
synchronized vs ReentrantLock
- 对比表
| 维度 | synchronized | ReentrantLock |
|---|---|---|
| 实现层次 | JVM 关键字 | JUC API |
| 自动释放 | 是 | 否 |
| 可中断获取 | 否 | 是 |
| 超时获取 | 否 | 是 |
| 公平锁 | 否 | 可选 |
| 多条件队列 | 否 | 支持 |
- 面试怎么答
- “默认优先 synchronized,因为语义简单、出错面小;只有在高级同步需求出现时才切到 ReentrantLock。”
- 易错点
- 公平锁并不一定更好,通常吞吐量更低。
ReentrantReadWriteLock 与 StampedLock
- 核心结论
- 读多写少场景下,读写分离锁有明显价值。
- 原理展开
ReentrantReadWriteLock允许多个读线程并发,但写线程独占。StampedLock提供乐观读,适合读远多于写且可接受重试的场景。
- 面试怎么答
- “如果只是传统读多写少,用 ReadWriteLock 就够;如果追求更高读性能且能处理校验失败重读,可以考虑 StampedLock。”
- 易错点
StampedLock不是可重入锁,使用方式和传统锁不同;乐观读拿到的只是一个 stamp,使用前后要校验。
3.3 volatile ★★★
volatile 的三个核心点
- 核心结论
- 保证可见性
- 禁止特定重排序
- 不保证复合操作原子性
- 原理展开
- 写
volatile变量后,JMM 会插入写屏障,让修改尽快对其他线程可见。 - 读
volatile变量前,JMM 会插入读屏障,确保之后读到的是主内存中的新值。 i++这类读-改-写是复合操作,即使i是volatile,也仍然可能竞争丢失更新。
- 写
- 面试怎么答
- “volatile 解决的是可见性和有序性,不是互斥。只要场景涉及多个线程对同一变量做复合更新,就不能只靠 volatile。”
- 常见追问
volatile和synchronized的根本区别是什么?- 为什么
volatile能禁止重排序?
典型使用场景
- 状态标志:例如停止线程、开关控制
- 单次写、多次读:例如配置刷新后的可见性
- DCL 单例中的引用发布
- 面试怎么答
- “volatile 最合适的场景是状态发布,不是计数器累加。”
DCL 单例为什么需要 volatile
- 原理展开
new Singleton()并不是原子操作,大致可拆为:- 分配内存
- 把引用指向这块内存
- 调用构造函数完成初始化
- 如果发生
1 -> 2 -> 3的重排序,其他线程可能读到一个“引用非空但对象未初始化完成”的半成品。
- 示例
public final class Singleton {
private static volatile Singleton INSTANCE;
private Singleton() {}
public static Singleton getInstance() {
if (INSTANCE == null) {
synchronized (Singleton.class) {
if (INSTANCE == null) {
INSTANCE = new Singleton();
}
}
}
return INSTANCE;
}
}- 面试怎么答
- “DCL 不是为了炫技,而是为了兼顾延迟初始化和并发安全。volatile 的关键作用不是可见性,而是禁止实例引用发布时的指令重排序。”
常见误区
volatile 不能替代锁。像 count++、余额扣减、先查再改这类复合更新,都需要 CAS、锁或原子类来保证正确性。
3.4 JMM(Java Memory Model) ★★★
JMM 解决什么问题
- 核心结论
- JMM 不是 JVM 内存布局图,而是 Java 的并发可见性与有序性规范。
- 它屏蔽了 CPU cache、编译器优化、处理器乱序执行的差异,给 Java 程序一个统一的并发语义。
- 原理展开
- 线程对共享变量的操作会先发生在自己的“工作内存”抽象中,再与“主内存”交互。
- 工作内存不是物理概念,可以理解成寄存器、CPU cache、编译器临时优化结果的统一抽象。
- 面试怎么答
- “JMM 的价值是定义什么时候一个线程写的数据对另一个线程可见,以及什么样的执行顺序对程序来说是合法的。”
主内存 vs 工作内存
- 核心结论
- 所有共享变量都存于主内存;线程操作时会把值拷到自己的工作内存再执行。
- 易错点
- 工作内存不是 JVM 堆里的一块真实区域,而是规范抽象。
happens-before 规则
- 核心结论
- happens-before 描述的是“如果 A happens-before B,那么 A 的结果对 B 可见,且 A 的执行顺序先于 B”。
- 高频规则
- 程序次序规则
- 监视器锁规则
- volatile 变量规则
- 线程启动规则
- 线程终止规则
- 线程中断规则
- 对象终结规则
- 传递性
- 面试怎么答
- “happens-before 不等于时间上绝对先执行,而是并发语义上的可见性与顺序保证。面试最常用的是锁规则、volatile 规则、start/join 规则和传递性。”
- 易错点
- 没有 happens-before 关系,不代表一定错,但意味着结果不可预测。
指令重排序与内存屏障
- 核心结论
- 编译器和 CPU 都会为了性能重排序,只要单线程语义不变。
- 并发程序需要靠内存屏障和同步原语约束这种重排。
- 原理展开
- 常见屏障类型包括
LoadLoad、LoadStore、StoreStore、StoreLoad。 StoreLoad开销最大,也是很多同步原语性能敏感的根源之一。
- 常见屏障类型包括
- 面试怎么答
- “JMM 不是禁止重排序,而是通过锁、volatile、final 发布等规则,让对程序正确性有影响的重排序不发生。”
3.5 JUC(java.util.concurrent) ★★★
AQS(AbstractQueuedSynchronizer)
- 核心结论
- AQS 是 JUC 里最重要的同步框架之一,本质是“一个
state状态值 + 一个等待队列 + 一套获取/释放模板方法”。 - 它不是直接提供锁,而是为上层同步器提供骨架。
- AQS 是 JUC 里最重要的同步框架之一,本质是“一个
- 原理展开
- 竞争失败的线程会进入一个 CLH 变体双向队列。
- 子类通过重写
tryAcquire/tryRelease/tryAcquireShared/tryReleaseShared定义同步语义。 - 独占模式下同一时刻只允许一个线程成功,例如
ReentrantLock; - 共享模式下可以允许多个线程同时成功,例如
Semaphore、CountDownLatch。
- 典型实现类
ReentrantLockReentrantReadWriteLockSemaphoreCountDownLatch
- 补充说明
CyclicBarrier很常和 AQS 一起被问,但它本身主要基于ReentrantLock + Condition实现,不是直接继承 AQS。
- 面试怎么答
- “AQS 把并发同步抽象成一个状态位和一个等待队列。上层同步器只需要定义如何尝试获取/释放资源,就能复用排队、阻塞、唤醒这套通用机制。”
- 常见追问
- 为什么 AQS 要用队列?
- 为什么
state用volatile? - CLH 和 MCS 有什么区别?
- 易错点
- 讲 AQS 时不要只说“volatile + CAS + 队列”,还要说明“谁用它、怎么扩展它”。
CAS 与原子类
- 核心结论
- CAS 是一种乐观并发控制:比较内存中的旧值是否仍然等于预期值,等于才更新。
- 它避免了重量级锁,但并非没有成本。
- 原理展开
- CAS 的典型问题是 ABA、自旋空转和单点热点。
- ABA 可通过版本号解决,如
AtomicStampedReference。 LongAdder把热点分散到多个 cell 上,减少高并发累加时的竞争。
- 面试怎么答
- “CAS 用失败重试换阻塞开销,适合冲突不高的场景。高冲突下它也会退化,所以像
LongAdder这种分段思路就很重要。”
- “CAS 用失败重试换阻塞开销,适合冲突不高的场景。高冲突下它也会退化,所以像
- 易错点
- 原子类只能保证单变量或单操作原子,不保证整段业务流程原子。
线程池(ThreadPoolExecutor) ★★★
- 核心结论
- 线程池的价值不只是复用线程,更重要的是控制并发度、隔离资源、平滑流量。
- 面试里一定要能把“7 大参数 + 执行流程 + 参数怎么定”一口气讲清楚。
- 7 大参数
corePoolSizemaximumPoolSizekeepAliveTimeunitworkQueuethreadFactoryRejectedExecutionHandler
- 工作流程
- 核心线程未满,先创建核心线程执行任务。
- 核心线程满了,任务进入队列。
- 队列满了,再创建非核心线程。
- 达到最大线程数后,触发拒绝策略。
- 参数设定思路
- CPU 密集型:线程数接近
CPU 核数 + 1 - IO 密集型:线程数可以更高,但要结合 IO 等待时间和下游承载能力
- 真正生产参数来自压测、监控、队列长度、任务耗时分布,而不是死背公式
- CPU 密集型:线程数接近
- 拒绝策略
AbortPolicyCallerRunsPolicyDiscardPolicyDiscardOldestPolicy
Executors工厂方法为什么常被批评FixedThreadPool/SingleThreadExecutor默认队列过大,容易堆积 OOMCachedThreadPool默认线程数近乎无上限,容易把机器打穿
- ForkJoinPool
- 适合可拆分任务,核心思想是工作窃取
parallel stream默认复用公共 ForkJoinPool,容易和其他异步任务互相干扰
- 面试怎么答
- “线程池不是越大越好。我的回答顺序通常是:先说 7 大参数,再说提交流程,再说如何按任务类型和下游容量定参数,最后补充拒绝策略和监控治理。”
- 常见追问
- 队列选有界还是无界?
- 线程池参数如何动态调整?
- 线程池满了怎么办?
- 易错点
- 不做监控的线程池等于埋雷。至少要看活跃线程数、队列长度、拒绝次数、任务耗时。
线程池回答抓手
如果面试官问“你们项目线程池怎么定的”,不要只说 CPU+1。更好的回答是:任务类型、平均耗时、峰值流量、下游超时、队列长度、拒绝策略、监控告警、压测校准。
并发工具类
CountDownLatchvsCyclicBarriervsSemaphore
| 工具 | 作用 | 是否可复用 | 典型场景 |
|---|---|---|---|
CountDownLatch | 等待计数归零 | 否 | 主线程等待多个子任务 |
CyclicBarrier | 一组线程互相等待后同时继续 | 是 | 分阶段计算、批次同步 |
Semaphore | 控制同时访问资源的数量 | 是 | 限流、连接池许可控制 |
CompletableFuture- 适合异步编排、结果组合和异常处理。
- 高频 API:
supplyAsync、thenApply、thenCompose、thenCombine、allOf。 - 风险点:默认线程池、链路中阻塞操作、异常被吞。
Phaser- 适合动态注册/注销参与者的分阶段协作场景。
Exchanger- 适合两个线程之间成对交换数据。
- 面试怎么答
- “并发工具类选型看同步语义:等一批任务结束用 CountDownLatch,等一批线程同时推进用 CyclicBarrier,控制并发数量用 Semaphore,编排异步流程用 CompletableFuture。”
3.6 并发容器
ConcurrentLinkedQueue / ConcurrentLinkedDeque
- 核心结论
- 这是无锁队列/双端队列,依赖 CAS 维护链表指针。
- 适合高并发、非阻塞、对吞吐更敏感的场景。
- 补充说明
- 无锁不等于零成本,竞争激烈时依然有 CAS 重试开销。
BlockingQueue 系列
- 核心结论
- 有界队列优先于无界队列,因为它天然提供背压能力。
- 典型实现
ArrayBlockingQueue:有界、数组、内存紧凑LinkedBlockingQueue:链表、吞吐常更高,但节点开销更大SynchronousQueue:不存元素,任务直接移交给消费者DelayQueue:按延迟到期顺序出队
- 面试怎么答
- “在线程池和生产者-消费者模型里,我优先考虑有界队列,因为系统需要明确的容量边界。”
CopyOnWriteArrayList / CopyOnWriteArraySet
- 核心结论
- 适合读多写少,读到旧数据可以接受的场景。
- 易错点
- 写放大非常明显,不适合实时热点更新。
3.7 ThreadLocal ★★
ThreadLocal 原理
- 核心结论
ThreadLocal不是“线程共享变量”,恰恰相反,它是“为每个线程提供独立副本”。
- 原理展开
- 每个线程对象内部维护一个
ThreadLocalMap。 ThreadLocal自身作为 key,实际值存到当前线程的 map 中。- key 是弱引用,value 是强引用,这是后面内存泄漏问题的根源。
- 每个线程对象内部维护一个
- 面试怎么答
- “ThreadLocal 的核心不是把值放在线程对象里,而是把‘以当前 Thread 为作用域的数据’绑定起来,典型场景是用户上下文、traceId、数据库连接等。”
内存泄漏原因与避免
- 原理展开
- 当
ThreadLocal对象本身没有强引用时,key 会被 GC 清掉。 - 但线程池中的工作线程往往长期存活,map 里的 value 仍然强引用着业务对象。
- 如果不主动
remove(),这些 value 可能长期残留。
- 当
- 示例
try {
USER_CONTEXT.set(userId);
// business logic
} finally {
USER_CONTEXT.remove();
}- 面试怎么答
- “ThreadLocal 泄漏不是因为弱引用本身,而是 key 被回收后 value 还挂在线程的 map 上。线程池线程不死,这些脏值就可能一直留着,所以必须
finally remove()。”
- “ThreadLocal 泄漏不是因为弱引用本身,而是 key 被回收后 value 还挂在线程的 map 上。线程池线程不死,这些脏值就可能一直留着,所以必须
InheritableThreadLocal 与 TTL
- 核心结论
InheritableThreadLocal只在“新建子线程”时复制父线程值,对线程池复用线程并不好用。- 跨线程池上下文传递更常见的做法是显式传参,或者使用
TransmittableThreadLocal这类工具,但要谨慎控制范围。
- 易错点
- ThreadLocal 只是上下文传递手段,不应滥用成全局变量替代品。
4. IO体系 ★★
4.1 传统IO
字节流 vs 字符流
- 核心结论
- 字节流处理原始二进制数据,字符流处理文本和字符编码。
- 补充说明
- 网络文件、图片、压缩包优先字节流。
- 文本处理要显式关注编码,不要依赖平台默认编码。
- 面试怎么答
- “字符流本质上也是在字节流之上做了编码解码封装。”
装饰器模式在 IO 中的体现
- 核心结论
- Java IO 大量使用装饰器模式,把功能分层叠加,而不是把所有能力堆进一个类。
- 典型例子
FileInputStream提供基础读取BufferedInputStream增加缓冲InputStreamReader增加字节到字符的解码
- 面试怎么答
- “IO API 看起来类很多,其实是在用装饰器把‘数据源’和‘增强能力’解耦,这也是经典设计模式的落地案例。”
序列化:Serializable、serialVersionUID、transient
- 核心结论
- JDK 原生序列化适合 JVM 内部对象持久化或少量兼容场景,不适合跨系统高性能通信。
- 关键点
serialVersionUID用来控制版本兼容。transient字段不会被默认序列化。- 原生序列化存在性能、体积和安全隐患,跨服务通信更常用 JSON、Protobuf、Hessian 等方案。
- 面试怎么答
- “原生序列化要知道能不能用,更要知道为什么生产里很多团队不用:性能一般、可读性差、兼容和安全成本高。”
4.2 NIO ★★
Buffer / Channel / Selector 三大核心
- 核心结论
- NIO 的思路不是“流式顺序读写”,而是“面向缓冲区和通道”的非阻塞 IO。
- 原理展开
Buffer是数据容器,核心属性是position、limit、capacity。Channel表示数据通道,可读可写。Selector负责多路复用,一个线程可以监听多个 Channel 的事件。
- 面试怎么答
- “NIO 的关键不是记类名,而是理解它把 IO 拆成了缓冲区、通道和事件分发,从而支持一个线程管理多个连接。”
- 易错点
flip()、clear()、rewind()是 ByteBuffer 高频坑点,面试里很喜欢问。
直接内存 vs 堆内存
- 核心结论
- 直接内存不在 Java 堆里,适合和本地 IO 交互频繁的场景。
- 对比表
| 维度 | 堆内存 ByteBuffer | 直接内存 ByteBuffer |
|---|---|---|
| 分配成本 | 较低 | 较高 |
| GC 管理 | 直接受 GC 管理 | 释放更复杂 |
| IO 拷贝 | 往往需要额外拷贝 | 更适合直接与内核交互 |
| 典型场景 | 普通对象操作 | NIO、Netty、文件/网络高性能 IO |
- 面试怎么答
- “直接内存的核心价值是减少一次用户态到内核态之间的拷贝成本,但分配和释放都比堆内存更重,不能滥用。”
零拷贝
- 核心结论
- 零拷贝不是完全没有数据移动,而是尽量减少 CPU 参与的拷贝和上下文切换。
- 常见方式
mmapsendfileFileChannel.transferTo()
- 场景理解
- Kafka、Netty、Nginx 这类高性能组件都会利用零拷贝优化文件到网络的传输路径。
- 面试怎么答
- “零拷贝的重点是减少用户态和内核态之间的复制,以及减少上下文切换,不是字面意义的一次都不拷。”
4.3 AIO(NIO.2)
AIO 的定位
- 核心结论
- AIO 是异步非阻塞模型,操作系统在 IO 完成后回调通知应用。
- 原理展开
- Java NIO.2 提供了
AsynchronousSocketChannel、CompletionHandler等 API。 - 它在理论上比 NIO 更贴近“提交任务后等回调”的模型。
- Java NIO.2 提供了
- BIO / NIO / AIO 对比
| 模型 | 同步/异步 | 阻塞/非阻塞 | 典型特点 |
|---|---|---|---|
| BIO | 同步 | 阻塞 | 一连接一线程,简单但扩展差 |
| NIO | 同步 | 非阻塞 | 多路复用,生产主流 |
| AIO | 异步 | 非阻塞 | 回调式,OS 支持差异大 |
- 面试怎么答
- “AIO 概念上更先进,但传统 Linux 生态里 NIO 更成熟,所以 Java 服务端生产里长期还是 NIO / Netty 更主流。”
- 易错点
- 面试不要把 AIO 简单说成“更高级的 NIO”,要补一句“落地效果依赖 OS 实现和生态成熟度”。
5. Java新特性 ★★
5.1 Java 8(必须掌握)
Lambda 表达式与函数式接口
- 核心结论
- Lambda 让“行为”可以像数据一样传递。
- 函数式接口指“只有一个抽象方法”的接口,例如
Runnable、Comparator、Function。
- 原理展开
- Lambda 并不是匿名内部类的简单语法糖,底层更接近
invokedynamic和 LambdaMetafactory。 - 方法引用是 Lambda 的一种更简洁写法,但前提是语义足够清晰。
- Lambda 并不是匿名内部类的简单语法糖,底层更接近
- 面试怎么答
- “Java 8 的真正变化不是写法更短,而是它让集合处理、回调、流式组合的表达能力大幅提升。”
- 易错点
- Lambda 捕获的局部变量必须是 effectively final。
Stream API
- 核心结论
- Stream 把集合处理抽象成一条数据管道:数据源 -> 中间操作 -> 终端操作。
- 它追求的是可读性与声明式表达,不是任何场景都比 for 循环更快。
- 原理展开
- 中间操作是惰性的,只有终端操作触发才真正执行。
- 常见中间操作:
filter、map、flatMap、sorted - 常见终端操作:
collect、forEach、reduce - 并行流底层使用公共 ForkJoinPool,适合纯 CPU 计算且无共享可变状态的场景。
- 面试怎么答
- “Stream 的价值是把数据处理流程写成声明式管道,但我会警惕并行流、共享状态和装箱开销,不会机械地把所有循环都改成 Stream。”
- 常见追问
- 为什么说 Stream 是惰性求值?
map和flatMap区别?- parallel stream 的坑有哪些?
- 易错点
- 在 Stream 中做有副作用的操作,尤其是并行流,是高频坑。
Optional
- 核心结论
- Optional 主要用于表达“值可能不存在”这件事,减少显式
null判断。
- Optional 主要用于表达“值可能不存在”这件事,减少显式
- 正确使用姿势
- 作为返回值表达可能为空
- 链式组合
map/flatMap/orElseGet
- 不推荐用法
- 作为实体类字段
- 作为 RPC / JPA 持久化对象属性
- 滥用
get(),那等于把问题推迟到运行时
- 面试怎么答
- “Optional 不是为了替代所有 null,而是让边界更清晰。真正体现水平的是知道它该用在哪,不该用在哪。”
CompletableFuture
- 核心结论
- 它是 Java 8 异步编排最重要的 API 之一,既是
Future,又是可编排的 CompletionStage。
- 它是 Java 8 异步编排最重要的 API 之一,既是
- 高频能力
- 串行编排:
thenApply、thenCompose - 并行组合:
thenCombine、allOf - 异常处理:
exceptionally、handle - 超时控制:
orTimeout、completeOnTimeout
- 串行编排:
- 面试怎么答
- “CompletableFuture 的价值不只是异步执行,而是把多步骤异步流程、异常传播、结果组合写成可维护的链式结构。”
- 易错点
- 默认线程池可能和其他公共任务互相影响,生产里经常要指定自定义线程池。
日期时间 API
- 核心结论
- Java 8 时间 API 的核心改进是不可变、线程安全、语义清晰。
- 常用类型
LocalDate:日期LocalDateTime:日期 + 时间ZonedDateTime:带时区Instant:时间戳语义Duration/Period:时间间隔
- 面试怎么答
- “新的时间 API 把旧版
Date/Calendar的可变和线程不安全问题基本都解决了,项目里应优先使用。”
- “新的时间 API 把旧版
- 易错点
LocalDateTime不带时区,跨时区场景要显式处理ZoneId。
5.2 Java 9-17(了解即可,加分项)
模块系统(JPMS)
- 核心结论
- 通过
module-info.java显式声明依赖和导出边界,解决大型工程的模块可见性问题。
- 通过
- 面试怎么答
- “知道它的目标是强封装和模块化即可,业务项目里是否全面采用要看历史包袱和生态兼容。”
var 局部变量类型推断
- 核心结论
var只是让编译器推断局部变量类型,不是动态类型。
- 易错点
- 滥用
var会降低可读性,尤其是右侧表达式不够直观时。
- 滥用
Switch 表达式
- 核心结论
- 支持
->语法和返回值,减少旧式switch的穿透和样板代码。
- 支持
- 面试怎么答
- “这类新语法的价值更多是提升表达力和减少错误,不是底层性能革新。”
Text Blocks
- 适合多行字符串,例如 JSON、SQL、模板文本。
- 能明显降低转义噪音。
Record
- 核心结论
- 适合承载不可变数据,自动生成构造器、访问器、
equals/hashCode/toString。
- 适合承载不可变数据,自动生成构造器、访问器、
- 面试怎么答
- “Record 适合 DTO、值对象,不适合复杂行为和可变领域对象。”
Sealed Classes
- 核心结论
- 显式限制哪些类可以继承某个父类,适合表达封闭层次结构。
- 场景
- 状态机、编译器 AST、消息类型分支。
Pattern Matching (instanceof)
- 核心结论
- 把类型判断和变量绑定合在一起,减少样板强转代码。
5.3 Java 17-21(前沿加分)
Virtual Threads
- 核心结论
- 虚拟线程把“线程”从昂贵的 OS 资源抽象成更轻量的调度单元,特别适合大量阻塞 IO 场景。
- 面试怎么答
- “虚拟线程提升的是并发规模和编程模型体验,不等于所有阻塞都没成本,也不意味着 CPU 密集型任务会神奇变快。”
- 易错点
- ThreadLocal、同步阻塞点、底层 native 调用都可能影响收益。
Structured Concurrency
- 核心结论
- 它强调把一组相关并发任务作为一个结构化作用域管理,统一取消、异常传播和生命周期。
- 面试怎么答
- “本质上是让并发代码更像结构化程序,降低‘子任务失控’问题。”
Foreign Function & Memory API
- 核心结论
- 目标是替代 JNI 中许多繁琐和危险的用法,让 Java 更安全地调用本地库和操作堆外内存。
- 面试怎么答
- “知道方向即可:这是 Java 和本地生态交互能力的现代化升级,实际业务里还要看框架和 JDK 版本接受度。”
二、高频面试题
使用方式
先遮住答案自己口述 30 秒,再对照关键词补漏。如果某题只能背结论、讲不出原理,说明还要回看正文对应知识点。
基础级(P7必答)
- HashMap 的 put 流程是怎样的?扩容机制?为什么负载因子是 0.75?
- 30秒答法:先做 hash 扰动,再按
(n-1)&hash定位桶位;无冲突直接放,有冲突则链表/红黑树插入;超过阈值后 resize。0.75 是时间和空间的折中。 - 关键词:数组 + 链表/红黑树、2 的幂、resize、oldCap 拆分、0.75
- 追问提醒:为什么树化阈值是 8?为什么容量必须是 2 的幂?
- ConcurrentHashMap 在 JDK 7 和 8 中的实现有什么区别?
- 30秒答法:JDK 7 用 Segment 分段锁;JDK 8 去掉 Segment,改成数组 + CAS + synchronized 的桶级并发控制,锁粒度更细。
- 关键词:Segment、CAS、桶头锁、CounterCell、协助扩容
- 追问提醒:为什么 JDK 8 还要用 synchronized?为什么不支持 null?
- synchronized 的锁升级过程?
- 30秒答法:无竞争时尽量走轻量级路径,竞争增加时从低成本状态升级到更重的监视器互斥。经典表述是偏向锁 -> 轻量级锁 -> 重量级锁。
- 关键词:Mark Word、CAS、自旋、Monitor、锁膨胀
- 追问提醒:为什么要做锁升级?偏向锁在新 JDK 里有什么变化?
- volatile 能保证线程安全吗?DCL 单例中为什么需要 volatile?
- 30秒答法:volatile 只保证可见性和有序性,不保证复合操作原子性。DCL 中需要它来禁止对象引用发布时的重排序,避免读到半初始化对象。
- 关键词:可见性、重排序、原子性、DCL、半初始化
- 追问提醒:volatile 和 synchronized 的根本差异是什么?
- 线程池的核心参数?任务提交后的执行流程?
- 30秒答法:7 大参数里最重要的是核心线程数、最大线程数、队列和拒绝策略。任务提交流程是:先占核心线程,再入队,队列满再扩线程,最后拒绝。
- 关键词:corePoolSize、maximumPoolSize、workQueue、handler、执行流程
- 追问提醒:参数怎么定?为什么不推荐 Executors?
- AQS 的原理?CountDownLatch 和 CyclicBarrier 的区别?
- 30秒答法:AQS 用
state + 队列 + 模板方法抽象同步器。CountDownLatch 是一次性倒计数,CyclicBarrier 是一组线程互等后继续,能循环使用。 - 关键词:state、CLH 变体队列、独占/共享、一次性、可复用
- 追问提醒:哪些类直接基于 AQS?CyclicBarrier 为什么不算直接 AQS 实现?
- ThreadLocal 原理?为什么会内存泄漏?
- 30秒答法:每个线程内部持有一个 ThreadLocalMap,key 是 ThreadLocal,value 是线程局部值。key 是弱引用、value 是强引用,线程池线程长期存活时若不
remove()就可能泄漏。 - 关键词:ThreadLocalMap、弱引用 key、强引用 value、线程池、remove
- 追问提醒:InheritableThreadLocal 为什么在线程池里不靠谱?
- Java 中有哪些引用类型?各自的 GC 行为?
- 30秒答法:强引用不会被 GC 回收;软引用在内存不足时回收;弱引用下次 GC 就可能回收;虚引用不影响生命周期,只用于回收通知。
- 关键词:强软弱虚、ReferenceQueue、缓存、回收通知
- 追问提醒:软引用适不适合作为稳定缓存?
- Stream 的惰性求值是怎么实现的?parallel stream 的坑?
- 30秒答法:中间操作不会立刻执行,只是组装管道,终端操作才触发遍历。parallel stream 默认用公共 ForkJoinPool,容易出现共享线程池争用和副作用问题。
- 关键词:中间操作、终端操作、惰性、ForkJoinPool.commonPool、副作用
- 追问提醒:map 和 flatMap 的区别?parallel stream 什么时候适合?
- String.intern() 在不同 JDK 版本中的区别?
- 30秒答法:早期 JDK 常量池在永久代,intern 更接近复制;JDK 7+ 常量池放到堆中,intern 更接近返回或复用池中引用。
- 关键词:常量池、永久代、堆、规范化引用
- 追问提醒:
new String("a")创建几个对象?
- ArrayList 和 LinkedList 的底层结构?什么场景选哪个?
- 30秒答法:ArrayList 是动态数组,随机访问和尾插好;LinkedList 是双向链表,双端插删语义更自然。真实业务里默认优先 ArrayList。
- 关键词:动态数组、双向链表、CPU cache、随机访问、Deque
- 追问提醒:为什么理论上链表插删快,但实际常常更慢?
- checked 和 unchecked 异常的区别?什么时候用 checked 异常?
- 30秒答法:checked 异常要求调用方显式处理,适合调用方确实能恢复的场景;unchecked 更适合编程错误或统一异常治理。
- 关键词:编译期强制、可恢复、RuntimeException、统一异常处理
- 追问提醒:业务异常到底该用 checked 还是 unchecked?
- CAS 的 ABA 问题是什么?怎么解决?
- 30秒答法:CAS 只比较当前值,A 变成 B 再变回 A 时,它看不出中间发生过变化。常见解法是引入版本号,例如
AtomicStampedReference。 - 关键词:ABA、版本号、AtomicStampedReference、自旋
- 追问提醒:CAS 为什么不一定比锁快?
- synchronized 和 ReentrantLock 的区别?什么时候选 ReentrantLock?
- 30秒答法:synchronized 是 JVM 关键字,简单且自动释放;ReentrantLock 是 API 级锁,支持可中断、超时、公平和多条件队列。只有需要这些高级能力时才优先选它。
- 关键词:自动释放、可中断、超时、公平锁、Condition
- 追问提醒:公平锁为什么吞吐更低?
- 泛型擦除是什么?如何在运行时获取泛型类型?
- 30秒答法:Java 泛型主要在编译期检查,运行期大多会擦除。运行时如果要保留泛型信息,常通过父类/接口签名或
TypeReference这类技巧间接拿到。 - 关键词:类型擦除、桥接方法、ParameterizedType、TypeReference
- 追问提醒:为什么不能
new T()?List<Object>和List<?>的区别?
- 反射的性能影响及优化策略?
- 30秒答法:反射慢在权限检查、方法查找、装箱拆箱和难以内联优化。常见优化是缓存元数据、使用
MethodHandle、或者直接用字节码增强替代热点反射。 - 关键词:Class 元数据、Method.invoke、缓存、MethodHandle、ASM/CGLIB
- 追问提醒:Spring/MyBatis 为什么仍大量使用反射?
- 自定义注解的实现与应用?
- 30秒答法:先用
@interface定义注解,再用@Target和@Retention约束它的作用范围和生命周期,最后由框架或反射逻辑去解析执行。 - 关键词:
@interface、@Target、@Retention(RUNTIME)、反射解析 - 追问提醒:注解为什么只是元数据?Spring 如何让注解生效?
- HashSet 底层实现?如何保证元素唯一性?
- 30秒答法:HashSet 底层就是 HashMap,元素放 key,value 是固定占位对象。唯一性靠
hashCode()定位 +equals()最终判等。 - 关键词:HashMap、PRESENT、hashCode、equals
- 追问提醒:为什么重写
equals()必须同时重写hashCode()?
- TreeMap 和 HashMap 的区别?红黑树如何自平衡?
- 30秒答法:HashMap 无序,均摊查找快;TreeMap 基于红黑树,有序且支持范围操作。红黑树通过旋转和变色维持近似平衡。
- 关键词:红黑树、有序、范围查询、旋转、变色
- 追问提醒:为什么 HashMap 不直接全用红黑树?
- 线程生命周期的六个状态?如何转换?
- 30秒答法:NEW -> RUNNABLE -> BLOCKED / WAITING / TIMED_WAITING -> TERMINATED。关键要分清是在等锁还是在等条件。
- 关键词:RUNNABLE、BLOCKED、WAITING、TIMED_WAITING、TERMINATED
- 追问提醒:BLOCKED 和 WAITING 的根本区别是什么?
- 线程池的拒绝策略有哪些?如何自定义?
- 30秒答法:内置有 Abort、CallerRuns、Discard、DiscardOldest 四种。自定义拒绝策略的核心是明确“被拒绝的任务怎么办”,例如记录日志、降级、落库或告警。
- 关键词:AbortPolicy、CallerRuns、Discard、DiscardOldest、RejectedExecutionHandler
- 追问提醒:为什么 CallerRuns 有时能起到背压效果?
- BIO、NIO、AIO 的区别?Netty 如何使用 NIO?
- 30秒答法:BIO 同步阻塞、一连接一线程;NIO 同步非阻塞、靠 Selector 多路复用;AIO 异步非阻塞、回调模型。Netty 基于 NIO/Reactor 封装高性能网络框架。
- 关键词:阻塞、非阻塞、多路复用、Selector、Reactor
- 追问提醒:为什么生产里 NIO/Netty 比 AIO 更常见?
- 枚举单例为什么是最安全的单例实现?
- 30秒答法:枚举实例由 JVM 保证唯一,天然防反射破坏,也天然兼容序列化语义,所以它几乎把普通单例的几个坑一次性解决了。
- 关键词:类加载、反射防护、序列化安全、实例唯一
- 追问提醒:枚举单例和 DCL 单例各自适合什么场景?
- CopyOnWriteArrayList 原理及适用场景?
- 30秒答法:写时复制,读直接读快照。适合读多写少、弱一致可接受的场景,例如监听器列表、配置快照。
- 关键词:快照、写复制、读无锁、弱一致、读多写少
- 追问提醒:为什么不适合高频写入?
- Hashtable 和 HashMap 的区别?为什么不推荐 Hashtable?
- 30秒答法:Hashtable 是整表同步、性能差、不支持 null;HashMap 非线程安全但轻量;并发场景更常用锁粒度更细的 ConcurrentHashMap。
- 关键词:整表锁、null、性能、ConcurrentHashMap
- 追问提醒:Hashtable 为什么在现代代码里基本消失?
- NIO Selector 的作用?epoll 和 select 的区别?
- 30秒答法:Selector 让一个线程监听多个 Channel 事件,实现 IO 多路复用。底层 OS 上 select/poll 需要遍历全部 fd,而 epoll 基于事件通知,扩展性更好。
- 关键词:多路复用、就绪事件、select/poll、epoll、事件驱动
- 追问提醒:NIO 为什么能支撑高并发连接?
- 如何用反射绕过泛型检查?有什么风险?
- 30秒答法:因为运行期泛型被擦除,反射拿到的
add(Object)仍可调用,所以可以把错误类型塞进集合。风险是把编译期问题拖成运行时ClassCastException。 - 关键词:类型擦除、反射、
add(Object)、ClassCastException - 追问提醒:框架为什么还要想办法保留泛型信息?
进阶级(P8+深挖)
- 如何设计一个线程安全的 LRU 缓存?有哪些实现方案?
- 30秒答法:简单方案是
LinkedHashMap + 锁,高并发方案是ConcurrentHashMap + 双向链表 + 分段控制,生产里更推荐直接用 Caffeine。 - 关键词:LinkedHashMap、访问顺序、并发控制、Caffeine、淘汰策略
- 追问提醒:为什么自己手写 LRU 往往不如直接用成熟缓存库?
- ConcurrentHashMap 的 size() 为什么不是精确的?如何保证精确计数?
- 30秒答法:高并发下 size 通过 baseCount 和 CounterCell 近似统计,避免全局锁热点。若一定要强一致统计,就要付出额外同步成本。
- 关键词:baseCount、CounterCell、近似统计、热点、强一致成本
- 追问提醒:
mappingCount()和size()有什么差别?
- AQS 为什么用 CLH 队列变体而不是 MCS 队列?
- 30秒答法:AQS 场景里需要更方便地处理取消、超时、唤醒后继和共享模式,所以采用了更适合 JVM 同步器实现的 CLH 变体双向队列。
- 关键词:CLH 变体、双向队列、取消、共享模式、唤醒后继
- 追问提醒:AQS 队列为什么还要维护
waitStatus?
- 为什么 JDK 8 HashMap 引入红黑树?为什么不直接全用红黑树?阈值 8 的依据?
- 30秒答法:链表在高碰撞下会退化为 O(n),红黑树能把最坏情况优化到 O(logn)。但红黑树节点更重、维护更复杂,所以只有碰撞链很长且数组容量足够大时才树化。
- 关键词:碰撞退化、O(n)、O(logn)、树化阈值 8、最小容量 64
- 追问提醒:为什么退化阈值是 6?为什么不是一有冲突就树化?
- CompletableFuture 在大量异步任务编排中如何避免线程饥饿?
- 30秒答法:避免直接依赖公共线程池,尽量指定自定义线程池;不要在异步链里做阻塞调用;加超时、限流和熔断,避免任务越堆越多。
- 关键词:自定义线程池、公共池、阻塞调用、超时、限流
- 追问提醒:什么情况下 CompletableFuture 反而让系统更难排障?
三、实战场景题(P8+重点)
- 线上 OOM 排查:你遇到过哪些 OOM 场景?如何排查和解决?
- 回答框架
- 先区分类型:堆 OOM、元空间 OOM、直接内存 OOM、线程过多导致无法创建线程。
- 再讲证据:GC 日志、heap dump、
jcmd/jmap/ Arthas、监控曲线。 - 再讲定位:对象暴涨、缓存失控、集合误用、类加载器泄漏、直接内存未释放。
- 最后讲治理:限流止血、参数调整、代码修复、压测验证、监控兜底。
- 并发问题定位:线上出现数据不一致,如何排查是并发问题?
- 回答框架
- 先定义“不一致”具体表现:重复、丢失、覆盖、顺序错乱。
- 排查共享资源:缓存、DB 行、分布式锁、消息重复消费、事务边界。
- 用日志和 trace 还原时序,必要时复现并加版本号/乐观锁/幂等键验证。
- 最后说明修复手段:加互斥、CAS、事务边界调整、幂等设计、重试策略改造。
- 线程池调优:你的项目中线程池参数是怎么确定的?有没有动态调整方案?
- 回答框架
- 先讲任务类型:CPU 密集、IO 密集、下游依赖特征。
- 再讲参数依据:峰值流量、平均耗时、队列长度、拒绝次数、压测结果。
- 再讲治理:线程池隔离、限流、熔断、监控告警。
- 动态调整可结合配置中心,但要强调变更边界和回滚策略。
- 大数据量处理:千万级数据导出,如何设计避免 OOM?
- 回答框架
- 不一次性把数据查进内存,改成分页、游标、流式处理。
- 结果输出用临时文件或对象存储,不要在内存里拼整份报表。
- 大任务异步化,前台只返回任务单号,后台生成后通知下载。
- 配套限流、并发控制、断点续传、失败重试和超时回收。
四、学习资源推荐
书籍
- 《Java并发编程的艺术》— 方腾飞:并发编程必读,AQS/JMM 讲解深入
- 《Java并发编程实战》— Brian Goetz:并发编程经典,偏工程实践
- 《Effective Java》(第3版) — Joshua Bloch:Java 语言最佳实践
- 《深入理解Java核心技术》— Hollis:集合、并发、源码级讲解更友好
官方资料
- OpenJDK JEP Index:适合查 Java 9+ 到 21 的语言特性背景
- dev.java / Oracle Java Docs:查标准库 API 和语言特性说明
- JSR-133(Java Memory Model) 相关资料:适合深入 JMM / happens-before
博客/文章
- 美团技术博客 — Java 并发、集合、JVM 实战文章
- 阿里技术公众号 — 源码与工程实践
- pdai.tech — 适合作为体系化回顾材料
视频
- 马士兵教育 — JUC 并发编程源码分析
- 黑马程序员 — Java 并发编程专题
- 各大厂技术公开课 / B 站源码拆解视频 — 适合补源码细节
使用建议
- 第一遍复习:先看正文,建立知识地图和回答结构
- 第二遍复习:用“高频面试题”做抽背
- 第三遍复习:结合你自己的项目经历,把每个重点知识点挂到一个真实场景上