面试指南

Java语言基础

面试权重:★★★ | 适用级别:P7/P8+ | 预计复习时间:2-3周

概览

面试权重:★★★ | 适用级别:P7/P8+ | 预计复习时间:2-3周

这一章是所有后续专题的底座。真正难的不是“背过多少点”,而是能否把语言机制、集合、并发和 JUC 串成一条完整的面试回答链路。

建议用法

先用正文建立理解,再用后面的“高频面试题”做抽背和口述训练。如果某个题讲不顺,优先回看对应正文块,而不是继续堆新题。

一、知识体系

1. Java基础

1.1 面向对象 ★★

封装、继承、多态

  • 核心结论
    • 封装的目标是隐藏实现细节、暴露稳定行为边界,本质是控制对象状态的可见性和修改方式。
    • 继承表达的是 is-a 关系,适合复用稳定抽象;如果只是为了偷懒复用代码,通常更应该考虑组合。
    • 多态的本质不是一句“父类引用指向子类对象”,而是“编译期看声明类型,运行期走实际对象的方法实现”。
  • 原理展开
    • Java 方法调用分静态绑定和动态绑定。staticprivatefinal 方法不会被重写,调用目标在编译期就能确定。
    • 普通实例方法依赖运行时分派,JVM 会基于实际对象类型找到最终实现。
    • 字段没有多态,字段访问取决于引用的编译期类型;发生动态分派的是可重写实例方法。
  • 面试怎么答
    • “封装解决边界和约束,继承解决稳定抽象复用,多态解决扩展替换。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() 契约,以及它对 HashMapHashSet 行为的影响。”
  • 常见追问
    • 为什么重写 equals() 必须同时重写 hashCode()
    • hashCode() 相同是否一定相等?
    • 自定义类作为 HashMap key 有什么要求?
  • 易错点
    • 可变对象不适合作为哈希 key。只要参与 equals() / hashCode() 的字段发生变化,后续查找就可能异常。

高频易错点

把可变业务对象直接作为 HashMap key,是线上常见坑。对象放入 map 后如果参与哈希计算的字段变了,后续就可能再也取不出来。

String 不可变性、String Pool、intern()
  • 核心结论
    • String 不可变的本质是:内部字符存储不对外暴露修改入口,值一旦确定就不能变。
    • 不可变带来的收益包括线程安全、可缓存哈希值、可作为 map key、可支持字符串常量池复用。
    • 字符串常量池不仅节省内存,也支撑了编译期常量折叠和运行时复用。
  • 原理展开
    • 现代 JDK 中 String 底层是 byte[] + coder,但“不可变”的抽象语义没有变。
    • 字面量 "abc" 在类加载阶段会进入字符串常量池;new String("abc") 会在堆上额外创建对象。
    • intern() 的作用是返回常量池中的规范化引用。JDK 7+ 开始常量池放在堆里,intern() 更接近“复用已有对象引用”,而不是早期版本那样复制一份。
  • 面试怎么答
    • “String 不可变并不只是 final 修饰,而是设计上不提供状态修改能力。它因此天然线程安全、适合做 key,也让字符串常量池成立。intern() 的本质是做字符串规范化引用,JDK 7+ 和 JDK 6 之前要分开说。”
  • 常见追问
    • 为什么 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 == btrue,因为命中了缓存。
    • Integer a = 128; Integer b = 128; 常常 a == bfalse,因为超过缓存范围。
    • 自动拆箱时如果对象为 null,会直接触发 NullPointerException,这是实际项目里的高频坑。
  • 面试怎么答
    • “自动装箱不是魔法,它只是编译器帮你插入了 valueOf()intValue()。真正面试高频的是 Integer 缓存和 null 拆箱 NPE。”
  • 易错点
    • 比较包装类值优先用 equals(),不要依赖 ==
反射机制
  • 核心结论
    • 反射让程序在运行期检查类型、创建对象、访问字段、调用方法,是大多数 Java 框架的基础设施。
    • 反射的价值是动态性,代价是类型安全更弱、性能较直接调用差、可读性较低。
  • 原理展开
    • JVM 加载类后会维护类元数据,Java 程序通过 Class 对象访问这些信息。
    • 常见入口包括 Class.forName()obj.getClass()SomeClass.class
    • 反射调用慢,主要慢在权限检查、方法查找、参数装箱拆箱,以及难以像静态调用那样充分内联优化。
    • Spring、MyBatis、Jackson 等框架会混合使用反射、动态代理、字节码增强,把灵活性和性能做平衡。
  • 面试怎么答
    • “反射本质是运行期操作类元数据。它让框架具备高度扩展能力,比如依赖注入、注解解析、ORM 映射,但高频路径通常要配合缓存、MethodHandle 或字节码增强。”
  • 常见追问
    • 反射为什么慢?
    • 反射和动态代理是什么关系?
    • 框架为什么离不开反射?
  • 易错点
    • 反射是工具,不是业务代码的默认手段;进入高频调用链后必须考虑缓存和替代方案。

注解(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 就可能回收。
    • 虚引用:不影响对象生命周期,只用于跟踪对象回收通知。
  • 面试怎么答
    • “四种引用的差别不在语法,而在对象的可回收性。最常落地的是缓存用软引用、ThreadLocalMap key 用弱引用、虚引用配合 ReferenceQueue 做资源回收跟踪。”
  • 易错点
    • 软引用不是稳定缓存方案,内存紧张时会被迅速清掉。

枚举与单例安全性

  • 核心结论
    • 枚举单例是最稳妥的单例实现之一,因为 JVM 天然保证枚举实例唯一。
  • 原理展开
    • 枚举实例在类加载阶段创建。
    • 枚举天生防反射破坏,也天然兼容序列化语义,不需要额外写 readResolve()
  • 面试怎么答
    • “如果只讨论单例安全性,枚举往往是最稳的实现,因为它同时解决了并发、反射和反序列化破坏的问题。”
  • 易错点
    • 枚举单例稳,不代表它适合所有场景;如果初始化成本高或依赖复杂,也要考虑装载时机。

面试回答提醒

讲泛型不要只背“类型擦除”四个字,最好顺着“编译期检查 -> 运行期擦除 -> 通配符边界 -> 框架如何保留类型信息”这条线回答,会更完整。

1.3 异常体系 ★★

Error vs Exception vs RuntimeException

  • 核心结论
    • Error 表示系统级严重问题,通常不要求业务代码恢复。
    • Exception 表示程序可以感知和处理的异常情况。
    • RuntimeException 是运行时异常,通常由编程错误或违反前置条件导致。
  • 原理展开
    • OutOfMemoryErrorStackOverflowError 属于 Error,重点是排查和治理,不是简单 catch
    • 受检异常(checked)要求调用方显式处理;非受检异常(unchecked)把处理权留给上层统一机制。
  • 面试怎么答
    • “三者差异不只是继承层次,而是处理哲学不同:Error 通常不可恢复,checked 异常强调强制处理,unchecked 更适合编程错误和统一异常治理。”

Checked vs Unchecked Exception

  • 核心结论
    • checked 异常适合调用方真正能恢复的场景。
    • unchecked 异常更适合参数错误、状态非法、系统内部 bug 或统一异常框架处理的场景。
  • 对比表
维度Checked ExceptionUnchecked Exception
编译期必须显式处理可不处理
典型场景IO、网络、外部资源失败空指针、越界、非法状态
设计意图强制调用方感知不把业务代码写成 try-catch 森林
  • 面试怎么答
    • “不是 checked 一定更严谨,也不是 unchecked 一定更高级,关键看调用方是否真的有可恢复动作。很多业务项目统一用 unchecked,再配合全局异常处理器,也是一种常见实践。”
  • 易错点
    • 不要把所有异常都包装成 RuntimeException,把上下文信息丢掉。

try-with-resources

  • 核心结论
    • 它本质是编译器语法糖,最终仍会展开成 try/finally 关闭资源。
    • 如果 try 块和 close() 都抛异常,try 块异常是主异常,关闭异常会作为 suppressed exception 附着。
  • 原理展开
    • 资源类必须实现 AutoCloseableCloseable
    • 这个语法最大的价值不是“少写 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,本质是尽早发现并发修改,不是线程安全机制,也不能保证绝对检测到。
  • 面试怎么答
    • “ArrayList 本质是动态数组,随机访问快、尾插均摊成本低。面试里除了说 1.5 倍扩容,还要补一句它常常比 LinkedList 更快,是因为连续内存更适合 CPU cache。”
  • 易错点
    • 频繁头插、中间插入、大批量扩容不预设容量,都会让性能明显变差。

LinkedList

  • 核心结论
    • LinkedList 基于双向链表,理论上插删节点成本低,但前提是“你已经拿到了目标节点”。
    • 如果需要先定位位置再插入/删除,真实成本往往不低。
  • 原理展开
    • 它每个节点都要存前后指针,内存开销更高,局部性更差。
    • LinkedList 同时实现了 ListDeque,在双端队列语义下比在随机访问语义下更合理。
  • 面试怎么答
    • “LinkedList 理论复杂度不差,但真实项目里因为节点分散、缓存不友好、内存开销高,通常只有在双端插删频繁时才值得选。”
  • 易错点
    • 不要因为“插入删除 O(1)”就草率选 LinkedList,真实业务里大多数时候都输给 ArrayList。

CopyOnWriteArrayList

  • 核心结论
    • 这是典型的读优化容器:读无锁、写复制。
    • 适合读远多于写、且可以接受短暂弱一致的场景。
  • 原理展开
    • 写操作先加锁复制底层数组,再在新数组上修改,最后替换引用。
    • 读操作直接读取当前数组快照,因此非常快,但可能读到旧数据。
  • 面试怎么答
    • “CopyOnWriteArrayList 用空间和写放大换读性能,适合监听器列表、黑白名单、配置快照这类读多写少场景,不适合高频写。”
  • 易错点
    • 大对象数组频繁写入会非常贵。

ArrayList vs LinkedList 真实性能对比

  • 核心结论
    • 绝大多数业务场景优先 ArrayList
  • 对比表
维度ArrayListLinkedList
底层结构动态数组双向链表
随机访问
尾部追加
中间插入需搬移元素找到节点后快
CPU 缓存友好
内存占用更紧凑更高
  • 面试怎么答
    • “默认选 ArrayList;只有双端操作特别多、确实受益于链表结构时才考虑 LinkedList。”

2.2 Map

HashMap ★★★
  • 核心结论
    • HashMap 是“数组 + 冲突桶结构”的混合体,JDK 8 后桶里可能是链表,也可能是红黑树。
    • 面试里最重要的不是背源码,而是把 put 流程、扩容机制、负载因子、树化条件串成一条线。
  • 原理展开
    • hash 过程会先拿 key.hashCode(),再做高位扰动,目的是让高位信息参与低位计算,减少碰撞。
    • 桶下标通常通过 (n - 1) & hash 计算,因此容量设为 2 的幂能让位运算既快又均匀。
    • put 大致流程:
      1. key 为 null 时走固定桶位逻辑。
      2. table 未初始化则先初始化。
      3. 定位桶位,无冲突直接放入。
      4. 有冲突则进入链表/红黑树处理。
      5. 插入后 size 超阈值则扩容。
    • JDK 8 扩容时不再对所有节点重新算 hash,而是根据 oldCap 那一位是 0 还是 1,把节点分到原位置或原位置 + oldCap,迁移成本更低。
    • 树化阈值是 8,退化阈值是 6,而且只有数组长度至少 64 才会树化;否则优先扩容。
  • 面试怎么答
    • “HashMap 底层是数组 + 链表/红黑树。put 时先扰动 hash 再按 (n-1)&hash 定位桶位,冲突时链表尾插或树插入,超过阈值扩容。容量是 2 的幂是为了让位运算定位更快更均匀,0.75 的负载因子则是时间和空间的折中。”
  • 常见追问
    • 为什么容量必须是 2 的幂?
    • 为什么负载因子是 0.75?
    • 为什么树化阈值是 8?
    • JDK 7 为什么并发扩容会死循环?
  • 易错点
    • HashMap 允许 null key 和 null value,但只允许一个 null key。
    • 它不是线程安全容器,并发写入时问题不只是结果不准,可能直接结构损坏。
    • 可变对象作为 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 的 ConcurrentHashMap 是 Segment 分段锁;JDK 8 变成数组 + CAS + synchronized 的桶级并发控制。核心改进是锁粒度更细,并且通过 CounterCell 等结构降低统计热点。”
  • 常见追问
    • JDK 8 为什么还用了 synchronized
    • 为什么 size() 不是绝对精确?
    • 为什么不直接给 HashMap 外面套一层 ReentrantLock?
  • 易错点
    • ConcurrentHashMap 不允许 null key/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。”
  • 易错点
    • 这只是单机内存级 LRU,实现简单但线程安全和淘汰策略能力有限,生产更常直接用 Caffeine。

HashMap / Hashtable / ConcurrentHashMap 对比

  • 核心结论
    • 业务开发里几乎不会优先选 Hashtable。
  • 对比表
维度HashMapHashtableConcurrentHashMap
线程安全
锁粒度整表更细粒度
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 后重新设置中断标记,不能悄悄吞掉。”
  • 易错点
    • 吞掉中断异常而不恢复标记,是线程池和框架代码中的典型 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
  • 对比表
维度synchronizedReentrantLock
实现层次JVM 关键字JUC API
自动释放
可中断获取
超时获取
公平锁可选
多条件队列支持
  • 面试怎么答
    • “默认优先 synchronized,因为语义简单、出错面小;只有在高级同步需求出现时才切到 ReentrantLock。”
  • 易错点
    • 公平锁并不一定更好,通常吞吐量更低。

ReentrantReadWriteLock 与 StampedLock

  • 核心结论
    • 读多写少场景下,读写分离锁有明显价值。
  • 原理展开
    • ReentrantReadWriteLock 允许多个读线程并发,但写线程独占。
    • StampedLock 提供乐观读,适合读远多于写且可接受重试的场景。
  • 面试怎么答
    • “如果只是传统读多写少,用 ReadWriteLock 就够;如果追求更高读性能且能处理校验失败重读,可以考虑 StampedLock。”
  • 易错点
    • StampedLock 不是可重入锁,使用方式和传统锁不同;乐观读拿到的只是一个 stamp,使用前后要校验。

3.3 volatile ★★★

volatile 的三个核心点
  • 核心结论
    • 保证可见性
    • 禁止特定重排序
    • 不保证复合操作原子性
  • 原理展开
    • volatile 变量后,JMM 会插入写屏障,让修改尽快对其他线程可见。
    • volatile 变量前,JMM 会插入读屏障,确保之后读到的是主内存中的新值。
    • i++ 这类读-改-写是复合操作,即使 ivolatile,也仍然可能竞争丢失更新。
  • 面试怎么答
    • “volatile 解决的是可见性和有序性,不是互斥。只要场景涉及多个线程对同一变量做复合更新,就不能只靠 volatile。”
  • 常见追问
    • volatilesynchronized 的根本区别是什么?
    • 为什么 volatile 能禁止重排序?

典型使用场景

  • 状态标志:例如停止线程、开关控制
  • 单次写、多次读:例如配置刷新后的可见性
  • DCL 单例中的引用发布
  • 面试怎么答
    • “volatile 最合适的场景是状态发布,不是计数器累加。”
DCL 单例为什么需要 volatile
  • 原理展开
    • new Singleton() 并不是原子操作,大致可拆为:
      1. 分配内存
      2. 把引用指向这块内存
      3. 调用构造函数完成初始化
    • 如果发生 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 都会为了性能重排序,只要单线程语义不变。
    • 并发程序需要靠内存屏障和同步原语约束这种重排。
  • 原理展开
    • 常见屏障类型包括 LoadLoadLoadStoreStoreStoreStoreLoad
    • StoreLoad 开销最大,也是很多同步原语性能敏感的根源之一。
  • 面试怎么答
    • “JMM 不是禁止重排序,而是通过锁、volatile、final 发布等规则,让对程序正确性有影响的重排序不发生。”

3.5 JUC(java.util.concurrent) ★★★

AQS(AbstractQueuedSynchronizer)
  • 核心结论
    • AQS 是 JUC 里最重要的同步框架之一,本质是“一个 state 状态值 + 一个等待队列 + 一套获取/释放模板方法”。
    • 它不是直接提供锁,而是为上层同步器提供骨架。
  • 原理展开
    • 竞争失败的线程会进入一个 CLH 变体双向队列。
    • 子类通过重写 tryAcquire / tryRelease / tryAcquireShared / tryReleaseShared 定义同步语义。
    • 独占模式下同一时刻只允许一个线程成功,例如 ReentrantLock
    • 共享模式下可以允许多个线程同时成功,例如 SemaphoreCountDownLatch
  • 典型实现类
    • ReentrantLock
    • ReentrantReadWriteLock
    • Semaphore
    • CountDownLatch
  • 补充说明
    • CyclicBarrier 很常和 AQS 一起被问,但它本身主要基于 ReentrantLock + Condition 实现,不是直接继承 AQS。
  • 面试怎么答
    • “AQS 把并发同步抽象成一个状态位和一个等待队列。上层同步器只需要定义如何尝试获取/释放资源,就能复用排队、阻塞、唤醒这套通用机制。”
  • 常见追问
    • 为什么 AQS 要用队列?
    • 为什么 statevolatile
    • CLH 和 MCS 有什么区别?
  • 易错点
    • 讲 AQS 时不要只说“volatile + CAS + 队列”,还要说明“谁用它、怎么扩展它”。
CAS 与原子类
  • 核心结论
    • CAS 是一种乐观并发控制:比较内存中的旧值是否仍然等于预期值,等于才更新。
    • 它避免了重量级锁,但并非没有成本。
  • 原理展开
    • CAS 的典型问题是 ABA、自旋空转和单点热点。
    • ABA 可通过版本号解决,如 AtomicStampedReference
    • LongAdder 把热点分散到多个 cell 上,减少高并发累加时的竞争。
  • 面试怎么答
    • “CAS 用失败重试换阻塞开销,适合冲突不高的场景。高冲突下它也会退化,所以像 LongAdder 这种分段思路就很重要。”
  • 易错点
    • 原子类只能保证单变量或单操作原子,不保证整段业务流程原子。
线程池(ThreadPoolExecutor) ★★★
  • 核心结论
    • 线程池的价值不只是复用线程,更重要的是控制并发度、隔离资源、平滑流量。
    • 面试里一定要能把“7 大参数 + 执行流程 + 参数怎么定”一口气讲清楚。
  • 7 大参数
    • corePoolSize
    • maximumPoolSize
    • keepAliveTime
    • unit
    • workQueue
    • threadFactory
    • RejectedExecutionHandler
  • 工作流程
    1. 核心线程未满,先创建核心线程执行任务。
    2. 核心线程满了,任务进入队列。
    3. 队列满了,再创建非核心线程。
    4. 达到最大线程数后,触发拒绝策略。
  • 参数设定思路
    • CPU 密集型:线程数接近 CPU 核数 + 1
    • IO 密集型:线程数可以更高,但要结合 IO 等待时间和下游承载能力
    • 真正生产参数来自压测、监控、队列长度、任务耗时分布,而不是死背公式
  • 拒绝策略
    • AbortPolicy
    • CallerRunsPolicy
    • DiscardPolicy
    • DiscardOldestPolicy
  • Executors 工厂方法为什么常被批评
    • FixedThreadPool / SingleThreadExecutor 默认队列过大,容易堆积 OOM
    • CachedThreadPool 默认线程数近乎无上限,容易把机器打穿
  • ForkJoinPool
    • 适合可拆分任务,核心思想是工作窃取
    • parallel stream 默认复用公共 ForkJoinPool,容易和其他异步任务互相干扰
  • 面试怎么答
    • “线程池不是越大越好。我的回答顺序通常是:先说 7 大参数,再说提交流程,再说如何按任务类型和下游容量定参数,最后补充拒绝策略和监控治理。”
  • 常见追问
    • 队列选有界还是无界?
    • 线程池参数如何动态调整?
    • 线程池满了怎么办?
  • 易错点
    • 不做监控的线程池等于埋雷。至少要看活跃线程数、队列长度、拒绝次数、任务耗时。

线程池回答抓手

如果面试官问“你们项目线程池怎么定的”,不要只说 CPU+1。更好的回答是:任务类型、平均耗时、峰值流量、下游超时、队列长度、拒绝策略、监控告警、压测校准。

并发工具类

  • CountDownLatch vs CyclicBarrier vs Semaphore
工具作用是否可复用典型场景
CountDownLatch等待计数归零主线程等待多个子任务
CyclicBarrier一组线程互相等待后同时继续分阶段计算、批次同步
Semaphore控制同时访问资源的数量限流、连接池许可控制
  • CompletableFuture
    • 适合异步编排、结果组合和异常处理。
    • 高频 API:supplyAsyncthenApplythenComposethenCombineallOf
    • 风险点:默认线程池、链路中阻塞操作、异常被吞。
  • 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()。”

InheritableThreadLocal 与 TTL

  • 核心结论
    • InheritableThreadLocal 只在“新建子线程”时复制父线程值,对线程池复用线程并不好用。
    • 跨线程池上下文传递更常见的做法是显式传参,或者使用 TransmittableThreadLocal 这类工具,但要谨慎控制范围。
  • 易错点
    • ThreadLocal 只是上下文传递手段,不应滥用成全局变量替代品。

4. IO体系 ★★

4.1 传统IO

字节流 vs 字符流

  • 核心结论
    • 字节流处理原始二进制数据,字符流处理文本和字符编码。
  • 补充说明
    • 网络文件、图片、压缩包优先字节流。
    • 文本处理要显式关注编码,不要依赖平台默认编码。
  • 面试怎么答
    • “字符流本质上也是在字节流之上做了编码解码封装。”

装饰器模式在 IO 中的体现

  • 核心结论
    • Java IO 大量使用装饰器模式,把功能分层叠加,而不是把所有能力堆进一个类。
  • 典型例子
    • FileInputStream 提供基础读取
    • BufferedInputStream 增加缓冲
    • InputStreamReader 增加字节到字符的解码
  • 面试怎么答
    • “IO API 看起来类很多,其实是在用装饰器把‘数据源’和‘增强能力’解耦,这也是经典设计模式的落地案例。”

序列化:SerializableserialVersionUIDtransient

  • 核心结论
    • JDK 原生序列化适合 JVM 内部对象持久化或少量兼容场景,不适合跨系统高性能通信。
  • 关键点
    • serialVersionUID 用来控制版本兼容。
    • transient 字段不会被默认序列化。
    • 原生序列化存在性能、体积和安全隐患,跨服务通信更常用 JSON、Protobuf、Hessian 等方案。
  • 面试怎么答
    • “原生序列化要知道能不能用,更要知道为什么生产里很多团队不用:性能一般、可读性差、兼容和安全成本高。”

4.2 NIO ★★

Buffer / Channel / Selector 三大核心
  • 核心结论
    • NIO 的思路不是“流式顺序读写”,而是“面向缓冲区和通道”的非阻塞 IO。
  • 原理展开
    • Buffer 是数据容器,核心属性是 positionlimitcapacity
    • Channel 表示数据通道,可读可写。
    • Selector 负责多路复用,一个线程可以监听多个 Channel 的事件。
  • 面试怎么答
    • “NIO 的关键不是记类名,而是理解它把 IO 拆成了缓冲区、通道和事件分发,从而支持一个线程管理多个连接。”
  • 易错点
    • flip()clear()rewind() 是 ByteBuffer 高频坑点,面试里很喜欢问。

直接内存 vs 堆内存

  • 核心结论
    • 直接内存不在 Java 堆里,适合和本地 IO 交互频繁的场景。
  • 对比表
维度堆内存 ByteBuffer直接内存 ByteBuffer
分配成本较低较高
GC 管理直接受 GC 管理释放更复杂
IO 拷贝往往需要额外拷贝更适合直接与内核交互
典型场景普通对象操作NIO、Netty、文件/网络高性能 IO
  • 面试怎么答
    • “直接内存的核心价值是减少一次用户态到内核态之间的拷贝成本,但分配和释放都比堆内存更重,不能滥用。”
零拷贝
  • 核心结论
    • 零拷贝不是完全没有数据移动,而是尽量减少 CPU 参与的拷贝和上下文切换。
  • 常见方式
    • mmap
    • sendfile
    • FileChannel.transferTo()
  • 场景理解
    • Kafka、Netty、Nginx 这类高性能组件都会利用零拷贝优化文件到网络的传输路径。
  • 面试怎么答
    • “零拷贝的重点是减少用户态和内核态之间的复制,以及减少上下文切换,不是字面意义的一次都不拷。”

4.3 AIO(NIO.2)

AIO 的定位

  • 核心结论
    • AIO 是异步非阻塞模型,操作系统在 IO 完成后回调通知应用。
  • 原理展开
    • Java NIO.2 提供了 AsynchronousSocketChannelCompletionHandler 等 API。
    • 它在理论上比 NIO 更贴近“提交任务后等回调”的模型。
  • BIO / NIO / AIO 对比
模型同步/异步阻塞/非阻塞典型特点
BIO同步阻塞一连接一线程,简单但扩展差
NIO同步非阻塞多路复用,生产主流
AIO异步非阻塞回调式,OS 支持差异大
  • 面试怎么答
    • “AIO 概念上更先进,但传统 Linux 生态里 NIO 更成熟,所以 Java 服务端生产里长期还是 NIO / Netty 更主流。”
  • 易错点
    • 面试不要把 AIO 简单说成“更高级的 NIO”,要补一句“落地效果依赖 OS 实现和生态成熟度”。

5. Java新特性 ★★

5.1 Java 8(必须掌握)

Lambda 表达式与函数式接口

  • 核心结论
    • Lambda 让“行为”可以像数据一样传递。
    • 函数式接口指“只有一个抽象方法”的接口,例如 RunnableComparatorFunction
  • 原理展开
    • Lambda 并不是匿名内部类的简单语法糖,底层更接近 invokedynamic 和 LambdaMetafactory。
    • 方法引用是 Lambda 的一种更简洁写法,但前提是语义足够清晰。
  • 面试怎么答
    • “Java 8 的真正变化不是写法更短,而是它让集合处理、回调、流式组合的表达能力大幅提升。”
  • 易错点
    • Lambda 捕获的局部变量必须是 effectively final。
Stream API
  • 核心结论
    • Stream 把集合处理抽象成一条数据管道:数据源 -> 中间操作 -> 终端操作。
    • 它追求的是可读性与声明式表达,不是任何场景都比 for 循环更快。
  • 原理展开
    • 中间操作是惰性的,只有终端操作触发才真正执行。
    • 常见中间操作:filtermapflatMapsorted
    • 常见终端操作:collectforEachreduce
    • 并行流底层使用公共 ForkJoinPool,适合纯 CPU 计算且无共享可变状态的场景。
  • 面试怎么答
    • “Stream 的价值是把数据处理流程写成声明式管道,但我会警惕并行流、共享状态和装箱开销,不会机械地把所有循环都改成 Stream。”
  • 常见追问
    • 为什么说 Stream 是惰性求值?
    • mapflatMap 区别?
    • parallel stream 的坑有哪些?
  • 易错点
    • 在 Stream 中做有副作用的操作,尤其是并行流,是高频坑。

Optional

  • 核心结论
    • Optional 主要用于表达“值可能不存在”这件事,减少显式 null 判断。
  • 正确使用姿势
    • 作为返回值表达可能为空
    • 链式组合 map / flatMap / orElseGet
  • 不推荐用法
    • 作为实体类字段
    • 作为 RPC / JPA 持久化对象属性
    • 滥用 get(),那等于把问题推迟到运行时
  • 面试怎么答
    • “Optional 不是为了替代所有 null,而是让边界更清晰。真正体现水平的是知道它该用在哪,不该用在哪。”
CompletableFuture
  • 核心结论
    • 它是 Java 8 异步编排最重要的 API 之一,既是 Future,又是可编排的 CompletionStage。
  • 高频能力
    • 串行编排:thenApplythenCompose
    • 并行组合:thenCombineallOf
    • 异常处理:exceptionallyhandle
    • 超时控制:orTimeoutcompleteOnTimeout
  • 面试怎么答
    • “CompletableFuture 的价值不只是异步执行,而是把多步骤异步流程、异常传播、结果组合写成可维护的链式结构。”
  • 易错点
    • 默认线程池可能和其他公共任务互相影响,生产里经常要指定自定义线程池。

日期时间 API

  • 核心结论
    • Java 8 时间 API 的核心改进是不可变、线程安全、语义清晰。
  • 常用类型
    • LocalDate:日期
    • LocalDateTime:日期 + 时间
    • ZonedDateTime:带时区
    • Instant:时间戳语义
    • Duration / Period:时间间隔
  • 面试怎么答
    • “新的时间 API 把旧版 Date / Calendar 的可变和线程不安全问题基本都解决了,项目里应优先使用。”
  • 易错点
    • 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必答)

  1. HashMap 的 put 流程是怎样的?扩容机制?为什么负载因子是 0.75?
  • 30秒答法:先做 hash 扰动,再按 (n-1)&hash 定位桶位;无冲突直接放,有冲突则链表/红黑树插入;超过阈值后 resize。0.75 是时间和空间的折中。
  • 关键词:数组 + 链表/红黑树、2 的幂、resize、oldCap 拆分、0.75
  • 追问提醒:为什么树化阈值是 8?为什么容量必须是 2 的幂?
  1. ConcurrentHashMap 在 JDK 7 和 8 中的实现有什么区别?
  • 30秒答法:JDK 7 用 Segment 分段锁;JDK 8 去掉 Segment,改成数组 + CAS + synchronized 的桶级并发控制,锁粒度更细。
  • 关键词:Segment、CAS、桶头锁、CounterCell、协助扩容
  • 追问提醒:为什么 JDK 8 还要用 synchronized?为什么不支持 null?
  1. synchronized 的锁升级过程?
  • 30秒答法:无竞争时尽量走轻量级路径,竞争增加时从低成本状态升级到更重的监视器互斥。经典表述是偏向锁 -> 轻量级锁 -> 重量级锁。
  • 关键词:Mark Word、CAS、自旋、Monitor、锁膨胀
  • 追问提醒:为什么要做锁升级?偏向锁在新 JDK 里有什么变化?
  1. volatile 能保证线程安全吗?DCL 单例中为什么需要 volatile?
  • 30秒答法:volatile 只保证可见性和有序性,不保证复合操作原子性。DCL 中需要它来禁止对象引用发布时的重排序,避免读到半初始化对象。
  • 关键词:可见性、重排序、原子性、DCL、半初始化
  • 追问提醒:volatile 和 synchronized 的根本差异是什么?
  1. 线程池的核心参数?任务提交后的执行流程?
  • 30秒答法:7 大参数里最重要的是核心线程数、最大线程数、队列和拒绝策略。任务提交流程是:先占核心线程,再入队,队列满再扩线程,最后拒绝。
  • 关键词:corePoolSize、maximumPoolSize、workQueue、handler、执行流程
  • 追问提醒:参数怎么定?为什么不推荐 Executors?
  1. AQS 的原理?CountDownLatch 和 CyclicBarrier 的区别?
  • 30秒答法:AQS 用 state + 队列 + 模板方法 抽象同步器。CountDownLatch 是一次性倒计数,CyclicBarrier 是一组线程互等后继续,能循环使用。
  • 关键词:state、CLH 变体队列、独占/共享、一次性、可复用
  • 追问提醒:哪些类直接基于 AQS?CyclicBarrier 为什么不算直接 AQS 实现?
  1. ThreadLocal 原理?为什么会内存泄漏?
  • 30秒答法:每个线程内部持有一个 ThreadLocalMap,key 是 ThreadLocal,value 是线程局部值。key 是弱引用、value 是强引用,线程池线程长期存活时若不 remove() 就可能泄漏。
  • 关键词:ThreadLocalMap、弱引用 key、强引用 value、线程池、remove
  • 追问提醒:InheritableThreadLocal 为什么在线程池里不靠谱?
  1. Java 中有哪些引用类型?各自的 GC 行为?
  • 30秒答法:强引用不会被 GC 回收;软引用在内存不足时回收;弱引用下次 GC 就可能回收;虚引用不影响生命周期,只用于回收通知。
  • 关键词:强软弱虚、ReferenceQueue、缓存、回收通知
  • 追问提醒:软引用适不适合作为稳定缓存?
  1. Stream 的惰性求值是怎么实现的?parallel stream 的坑?
  • 30秒答法:中间操作不会立刻执行,只是组装管道,终端操作才触发遍历。parallel stream 默认用公共 ForkJoinPool,容易出现共享线程池争用和副作用问题。
  • 关键词:中间操作、终端操作、惰性、ForkJoinPool.commonPool、副作用
  • 追问提醒:map 和 flatMap 的区别?parallel stream 什么时候适合?
  1. String.intern() 在不同 JDK 版本中的区别?
  • 30秒答法:早期 JDK 常量池在永久代,intern 更接近复制;JDK 7+ 常量池放到堆中,intern 更接近返回或复用池中引用。
  • 关键词:常量池、永久代、堆、规范化引用
  • 追问提醒:new String("a") 创建几个对象?
  1. ArrayList 和 LinkedList 的底层结构?什么场景选哪个?
  • 30秒答法:ArrayList 是动态数组,随机访问和尾插好;LinkedList 是双向链表,双端插删语义更自然。真实业务里默认优先 ArrayList。
  • 关键词:动态数组、双向链表、CPU cache、随机访问、Deque
  • 追问提醒:为什么理论上链表插删快,但实际常常更慢?
  1. checked 和 unchecked 异常的区别?什么时候用 checked 异常?
  • 30秒答法:checked 异常要求调用方显式处理,适合调用方确实能恢复的场景;unchecked 更适合编程错误或统一异常治理。
  • 关键词:编译期强制、可恢复、RuntimeException、统一异常处理
  • 追问提醒:业务异常到底该用 checked 还是 unchecked?
  1. CAS 的 ABA 问题是什么?怎么解决?
  • 30秒答法:CAS 只比较当前值,A 变成 B 再变回 A 时,它看不出中间发生过变化。常见解法是引入版本号,例如 AtomicStampedReference
  • 关键词:ABA、版本号、AtomicStampedReference、自旋
  • 追问提醒:CAS 为什么不一定比锁快?
  1. synchronized 和 ReentrantLock 的区别?什么时候选 ReentrantLock?
  • 30秒答法:synchronized 是 JVM 关键字,简单且自动释放;ReentrantLock 是 API 级锁,支持可中断、超时、公平和多条件队列。只有需要这些高级能力时才优先选它。
  • 关键词:自动释放、可中断、超时、公平锁、Condition
  • 追问提醒:公平锁为什么吞吐更低?
  1. 泛型擦除是什么?如何在运行时获取泛型类型?
  • 30秒答法:Java 泛型主要在编译期检查,运行期大多会擦除。运行时如果要保留泛型信息,常通过父类/接口签名或 TypeReference 这类技巧间接拿到。
  • 关键词:类型擦除、桥接方法、ParameterizedType、TypeReference
  • 追问提醒:为什么不能 new T()List<Object>List<?> 的区别?
  1. 反射的性能影响及优化策略?
  • 30秒答法:反射慢在权限检查、方法查找、装箱拆箱和难以内联优化。常见优化是缓存元数据、使用 MethodHandle、或者直接用字节码增强替代热点反射。
  • 关键词:Class 元数据、Method.invoke、缓存、MethodHandle、ASM/CGLIB
  • 追问提醒:Spring/MyBatis 为什么仍大量使用反射?
  1. 自定义注解的实现与应用?
  • 30秒答法:先用 @interface 定义注解,再用 @Target@Retention 约束它的作用范围和生命周期,最后由框架或反射逻辑去解析执行。
  • 关键词:@interface@Target@Retention(RUNTIME)、反射解析
  • 追问提醒:注解为什么只是元数据?Spring 如何让注解生效?
  1. HashSet 底层实现?如何保证元素唯一性?
  • 30秒答法:HashSet 底层就是 HashMap,元素放 key,value 是固定占位对象。唯一性靠 hashCode() 定位 + equals() 最终判等。
  • 关键词:HashMap、PRESENT、hashCode、equals
  • 追问提醒:为什么重写 equals() 必须同时重写 hashCode()
  1. TreeMap 和 HashMap 的区别?红黑树如何自平衡?
  • 30秒答法:HashMap 无序,均摊查找快;TreeMap 基于红黑树,有序且支持范围操作。红黑树通过旋转和变色维持近似平衡。
  • 关键词:红黑树、有序、范围查询、旋转、变色
  • 追问提醒:为什么 HashMap 不直接全用红黑树?
  1. 线程生命周期的六个状态?如何转换?
  • 30秒答法:NEW -> RUNNABLE -> BLOCKED / WAITING / TIMED_WAITING -> TERMINATED。关键要分清是在等锁还是在等条件。
  • 关键词:RUNNABLE、BLOCKED、WAITING、TIMED_WAITING、TERMINATED
  • 追问提醒:BLOCKED 和 WAITING 的根本区别是什么?
  1. 线程池的拒绝策略有哪些?如何自定义?
  • 30秒答法:内置有 Abort、CallerRuns、Discard、DiscardOldest 四种。自定义拒绝策略的核心是明确“被拒绝的任务怎么办”,例如记录日志、降级、落库或告警。
  • 关键词:AbortPolicy、CallerRuns、Discard、DiscardOldest、RejectedExecutionHandler
  • 追问提醒:为什么 CallerRuns 有时能起到背压效果?
  1. BIO、NIO、AIO 的区别?Netty 如何使用 NIO?
  • 30秒答法:BIO 同步阻塞、一连接一线程;NIO 同步非阻塞、靠 Selector 多路复用;AIO 异步非阻塞、回调模型。Netty 基于 NIO/Reactor 封装高性能网络框架。
  • 关键词:阻塞、非阻塞、多路复用、Selector、Reactor
  • 追问提醒:为什么生产里 NIO/Netty 比 AIO 更常见?
  1. 枚举单例为什么是最安全的单例实现?
  • 30秒答法:枚举实例由 JVM 保证唯一,天然防反射破坏,也天然兼容序列化语义,所以它几乎把普通单例的几个坑一次性解决了。
  • 关键词:类加载、反射防护、序列化安全、实例唯一
  • 追问提醒:枚举单例和 DCL 单例各自适合什么场景?
  1. CopyOnWriteArrayList 原理及适用场景?
  • 30秒答法:写时复制,读直接读快照。适合读多写少、弱一致可接受的场景,例如监听器列表、配置快照。
  • 关键词:快照、写复制、读无锁、弱一致、读多写少
  • 追问提醒:为什么不适合高频写入?
  1. Hashtable 和 HashMap 的区别?为什么不推荐 Hashtable?
  • 30秒答法:Hashtable 是整表同步、性能差、不支持 null;HashMap 非线程安全但轻量;并发场景更常用锁粒度更细的 ConcurrentHashMap。
  • 关键词:整表锁、null、性能、ConcurrentHashMap
  • 追问提醒:Hashtable 为什么在现代代码里基本消失?
  1. NIO Selector 的作用?epoll 和 select 的区别?
  • 30秒答法:Selector 让一个线程监听多个 Channel 事件,实现 IO 多路复用。底层 OS 上 select/poll 需要遍历全部 fd,而 epoll 基于事件通知,扩展性更好。
  • 关键词:多路复用、就绪事件、select/poll、epoll、事件驱动
  • 追问提醒:NIO 为什么能支撑高并发连接?
  1. 如何用反射绕过泛型检查?有什么风险?
  • 30秒答法:因为运行期泛型被擦除,反射拿到的 add(Object) 仍可调用,所以可以把错误类型塞进集合。风险是把编译期问题拖成运行时 ClassCastException
  • 关键词:类型擦除、反射、add(Object)、ClassCastException
  • 追问提醒:框架为什么还要想办法保留泛型信息?

进阶级(P8+深挖)

  1. 如何设计一个线程安全的 LRU 缓存?有哪些实现方案?
  • 30秒答法:简单方案是 LinkedHashMap + 锁,高并发方案是 ConcurrentHashMap + 双向链表 + 分段控制,生产里更推荐直接用 Caffeine。
  • 关键词:LinkedHashMap、访问顺序、并发控制、Caffeine、淘汰策略
  • 追问提醒:为什么自己手写 LRU 往往不如直接用成熟缓存库?
  1. ConcurrentHashMap 的 size() 为什么不是精确的?如何保证精确计数?
  • 30秒答法:高并发下 size 通过 baseCount 和 CounterCell 近似统计,避免全局锁热点。若一定要强一致统计,就要付出额外同步成本。
  • 关键词:baseCount、CounterCell、近似统计、热点、强一致成本
  • 追问提醒:mappingCount()size() 有什么差别?
  1. AQS 为什么用 CLH 队列变体而不是 MCS 队列?
  • 30秒答法:AQS 场景里需要更方便地处理取消、超时、唤醒后继和共享模式,所以采用了更适合 JVM 同步器实现的 CLH 变体双向队列。
  • 关键词:CLH 变体、双向队列、取消、共享模式、唤醒后继
  • 追问提醒:AQS 队列为什么还要维护 waitStatus
  1. 为什么 JDK 8 HashMap 引入红黑树?为什么不直接全用红黑树?阈值 8 的依据?
  • 30秒答法:链表在高碰撞下会退化为 O(n),红黑树能把最坏情况优化到 O(logn)。但红黑树节点更重、维护更复杂,所以只有碰撞链很长且数组容量足够大时才树化。
  • 关键词:碰撞退化、O(n)、O(logn)、树化阈值 8、最小容量 64
  • 追问提醒:为什么退化阈值是 6?为什么不是一有冲突就树化?
  1. CompletableFuture 在大量异步任务编排中如何避免线程饥饿?
  • 30秒答法:避免直接依赖公共线程池,尽量指定自定义线程池;不要在异步链里做阻塞调用;加超时、限流和熔断,避免任务越堆越多。
  • 关键词:自定义线程池、公共池、阻塞调用、超时、限流
  • 追问提醒:什么情况下 CompletableFuture 反而让系统更难排障?

三、实战场景题(P8+重点)

  1. 线上 OOM 排查:你遇到过哪些 OOM 场景?如何排查和解决?
  • 回答框架
    • 先区分类型:堆 OOM、元空间 OOM、直接内存 OOM、线程过多导致无法创建线程。
    • 再讲证据:GC 日志、heap dump、jcmd / jmap / Arthas、监控曲线。
    • 再讲定位:对象暴涨、缓存失控、集合误用、类加载器泄漏、直接内存未释放。
    • 最后讲治理:限流止血、参数调整、代码修复、压测验证、监控兜底。
  1. 并发问题定位:线上出现数据不一致,如何排查是并发问题?
  • 回答框架
    • 先定义“不一致”具体表现:重复、丢失、覆盖、顺序错乱。
    • 排查共享资源:缓存、DB 行、分布式锁、消息重复消费、事务边界。
    • 用日志和 trace 还原时序,必要时复现并加版本号/乐观锁/幂等键验证。
    • 最后说明修复手段:加互斥、CAS、事务边界调整、幂等设计、重试策略改造。
  1. 线程池调优:你的项目中线程池参数是怎么确定的?有没有动态调整方案?
  • 回答框架
    • 先讲任务类型:CPU 密集、IO 密集、下游依赖特征。
    • 再讲参数依据:峰值流量、平均耗时、队列长度、拒绝次数、压测结果。
    • 再讲治理:线程池隔离、限流、熔断、监控告警。
    • 动态调整可结合配置中心,但要强调变更边界和回滚策略。
  1. 大数据量处理:千万级数据导出,如何设计避免 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 站源码拆解视频 — 适合补源码细节

使用建议

  • 第一遍复习:先看正文,建立知识地图和回答结构
  • 第二遍复习:用“高频面试题”做抽背
  • 第三遍复习:结合你自己的项目经历,把每个重点知识点挂到一个真实场景上

On this page