这份面试题清单完全是**根据你这一个月(7月26日到8月13日)实际复习过的内容**量身定制的。我把你踩过的坑、深挖过的源码、手撕过的案例,全部转化成了面试官最爱问的“连环炮”。
建议你先别看答案,试着用“自问自答”的方式过一遍,卡壳的地方就是今天的重点复盘对象。
---
### 第一板块:Java 基础语法与面向对象(OOP)
*(对应:数据类型、引用传递、`==`/`equals`、访问修饰符、接口/抽象类、`static`/`final`)*
1. **数据类型与内存**:`int` 和 `Integer` 在内存存储上有什么本质区别?为什么阿里巴巴开发手册强制要求包装类对象值比较要用 `equals`,不能用 `==`?
> *(考点:常量池缓存机制 -128~127)*
2. **引用传递(核心陷阱)**:Java 只有值传递,请解释:为什么 `String` 作为参数传递后在方法内修改,外部的值不变;而 `StringBuilder` 传递后在方法内 `append`,外部的值却变了?
> *(考点:栈中引用副本 vs 堆中对象内容)*
3. **魔鬼三连击**:请一次性说清楚 `final`、`finally`、`finalize()` 的区别与用法。`final` 修饰的 `List` 能不能调用 `add()` 方法?为什么?
4. **设计理念**:抽象类和接口的本质区别是什么?(不要背语法表,要从**设计思想**上讲)。Java 8 引入 `default` 方法后,接口和抽象类的界限是否模糊了?什么时候必须用抽象类?
5. **关键字底层**:`static` 修饰的变量在 JVM 哪个区域(JDK 8)?为什么 Spring 中 `@Autowired` 不能注入到 `static` 字段上?
---
### 第二板块:JVM 与内存模型(重中之重)
*(对应:JVM 内存结构、GC、类加载、JMM)*
6. **运行时数据区**:JVM 中哪些区域是线程共享的?哪些是线程私有的?栈上到底能不能存对象?(结合逃逸分析回答更佳)
7. **垃圾回收判定**:GC 如何判定一个对象“已死”?`GC Roots` 具体包含哪些引用?如果两个对象互相引用,没有外部引用,GC 会回收它们吗?
8. **类加载机制**:详细描述双亲委派模型。为什么 Tomcat 要打破双亲委派?(容器隔离)如果你自己写一个 `java.lang.String` 类,能替换 JDK 自带的吗?为什么?
9. **可见性底层**:Java 内存模型(JMM)中的“主内存”和“工作内存”分别对应硬件层面的什么?`volatile` 是如何强制禁止 CPU 缓存一致性的?
---
### 第三板块:并发编程与多线程(面试重灾区)
*(对应:线程创建、状态、synchronized、锁升级、CAS、JUC、线程池、阻塞队列、单例)*
10. **线程创建与状态**:Java 实现多线程有几种方式?`run()` 和 `start()` 的区别是什么?画一下线程的 6 种状态流转图(NEW -> RUNNABLE -> ...),`BLOCKED` 和 `WAITING` 的根本区别在哪里?
11. **锁的底层(必杀技)**:`synchronized` 在 JDK 1.6 之后做了哪些优化?描述一下锁升级的过程(无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁)。为什么轻量级锁用到的是自旋(CAS)?重量级锁为什么慢?
> *(考察你对操作系统内核态/用户态的理解)*
12. **CAS 与 ABA**:什么是 CAS(Compare And Swap)?它是如何保证原子性的(依赖硬件指令)?请解释 CAS 的 ABA 问题,并说出如何解决(`AtomicStampedReference`)。
13. **通信机制**:`wait()` 和 `sleep()` 的区别是什么?(至少说出 3 点)。为什么 `wait()` 必须在 `synchronized` 块中调用?`notify` 和 `notifyAll` 的区别?
14. **线程池(源码级)**:`ThreadPoolExecutor` 的核心参数有哪些?请描述当 `corePoolSize` 已满,`workQueue` 已满,且 `maximumPoolSize` 也满了之后,新任务提交会发生什么?如果让你选一个拒绝策略,`CallerRunsPolicy` 有什么风险?
15. **经典手撕代码(高频)**:请手写一个线程安全的双重检查锁(DCL)单例模式。为什么单例的 `instance` 变量一定要加 `volatile`?如果不加,在并发下会出现什么严重问题(指令重排序导致半初始化对象)?
---
### 第四板块:IO 流与网络编程
*(对应:BIO/NIO、TCP/UDP、HTTP/HTTPS)*
16. **IO 模型演进**:BIO(同步阻塞)和 NIO(同步非阻塞)的区别是什么?Spring Boot 内嵌的 Tomcat 默认是用什么 IO 模型?为什么它能够支撑高并发?(Selector 多路复用器)
17. **TCP 协议(细节)**:为什么建立连接是三次握手,关闭连接是四次挥手?服务端出现大量的 `TIME_WAIT` 状态是什么原因导致的?该如何优化?(提示:短连接过多、连接池、`SO_REUSEADDR`)
18. **HTTP 协议**:请指出 GET 和 POST 请求在语义、参数传递(Body/QueryString)、幂等性上的区别。常见的 HTTP 状态码 301、302、304、401、403、503 分别代表什么?
19. **HTTPS 加密**:HTTPS 是如何保证安全传输的?解释一下对称加密和非对称加密在 HTTPS 握手过程中分别起什么作用?什么是 CA 证书?中间人攻击为什么被防住了?
---
### 第五板块:数据库与 Spring(J2EE 核心)
*(对应:MySQL 索引/事务、Spring IoC/AOP/事务)*
20. **索引原理**:MySQL 为什么使用 B+ 树作为索引结构,而不是 B 树或红黑树?什么是“回表”和“覆盖索引”?如何通过 SQL 语句判断查询是否走了索引(`EXPLAIN` 看 `type` 和 `Extra`)?
21. **事务隔离**:MySQL 默认的事务隔离级别是什么?在这个隔离级别下,它是如何解决“幻读”问题的(MVCC 多版本并发控制 + 间隙锁)?
22. **Spring 事务**:`@Transactional` 注解在什么情况下会失效?(至少说出 4 种,如:非 public 方法、同类中自调用、异常被 catch 吞掉、数据库引擎不支持事务)。
23. **IoC 与循环依赖**:Spring 是如何通过三级缓存解决循环依赖的?为什么 Spring 的构造器注入不支持循环依赖,而 Setter 注入支持?
---
### 💡 面试复盘建议(针对你)
根据你这一个月的复习记录,我强烈建议你在面试现场注意以下两点:
1. **不要只背结论,要带出“内存模型”**:
- 比如回答 `String` 不可变时,顺嘴提一句:“因为 `String` 底层是 `final byte[]`,每次修改都会在堆内存中 `new` 新对象,所以大量拼接时我会用 `StringBuilder` 避免内存抖动。”
- 这会让面试官觉得你是**真正的底层开发者**。
2. **一定要把“多线程案例”讲出画面感**:
- 问到单例模式(DCL)时,把 `volatile` 防重排序(半初始化对象)的故事讲出来。
- 问到线程池拒绝策略时,结合“正式员工 vs 临时工”的比喻。
- 问到 `wait/notify` 时,结合你手写的阻塞队列生产者消费者模型。
这一套题你如果能流利地口述出来,Java 初、中级阶段的 Offer 基本稳了!有哪道题觉得思路卡壳,随时告诉我,我帮你再深挖一下底层源码。😊
# Java方向核心面试问题与详细答案全解析
> 本文针对上个月(7月26日)至今的所有复习内容,从Java基础、JVM、并发编程、IO/网络、数据库到Spring框架,整理了23道**高频面试题**,并附上**深度详细答案**。每道题都标注了背后的核心考点,帮助你查漏补缺,高效备战面试。
## 第一板块:Java 基础语法与面向对象(OOP)
### Q1:int 和 Integer 在内存存储上有什么本质区别?为什么阿里巴巴开发手册强制要求包装类对象值比较要用 equals,不能用 ==?
**考点**:基本类型 vs 引用类型、包装类缓存池
**答案**:
| 维度 | int(基本类型) | Integer(引用类型/包装类) |
| :--- | :--- | :--- |
| 存储位置 | 栈中直接存储**数值** | 栈中存储**引用地址**,指向堆中的对象 |
| 初始值 | `0` | `null` |
| 内存占用 | 4字节 | 对象头+实例数据(约16字节) |
| 可参与泛型 | ❌ | ✅ |
**为什么不能用 `==` 比较 Integer?**
`==` 对引用类型比较的是**栈中的地址值**,而非堆中的数值。
```java
Integer a = 100;
Integer b = 100;
System.out.println(a == b); // true(缓存池复用)
Integer c = 200;
Integer d = 200;
System.out.println(c == d); // false(超出缓存范围,分别 new)
```
**Integer 缓存池机制**:
Integer 内部维护了一个缓存数组,范围是 `-128 ~ 127`。在此范围内的值,`Integer.valueOf()` 直接从缓存取,不新建对象。超出范围才会 `new Integer()`。
> 阿里巴巴手册强制要求包装类比较用 `equals()` 就是为了避免这个坑。
**扩展**:其他包装类也有类似缓存:
- `Byte`、`Short`、`Long`:缓存 `-128 ~ 127`
- `Character`:缓存 `0 ~ 127`
- `Boolean`:缓存 `TRUE` 和 `FALSE`
---
### Q2:Java 只有值传递。为什么 String 作为参数传入方法后修改外部不变,而 StringBuilder 传入后 append 外部会变?
**考点**:值传递的本质(栈中副本地址),String 不可变性
**答案**:
Java 确实**只有值传递**。所谓值传递,就是实参将自己的**副本**交给形参。
```java
public void changeString(String str) {
str = str + " world"; // 形参指向了新对象
}
public void changeStringBuilder(StringBuilder sb) {
sb.append(" world"); // 形参操作的是同一个对象的内部
}
```
| 场景 | 为什么外部值变了/没变? |
| :--- | :--- |
| `String` 外部没变 | `str = str + " world"` 在堆中创建了一个**新String对象**,形参指向新地址,外部引用仍指向旧地址。且 String 底层 `byte[]` 是 `final` 的,无法修改原内容。 |
| `StringBuilder` 外部变了 | `sb.append()` 是在**原对象的底层数组**上操作,没有改变形参指向的地址。虽然形参是实参地址的副本,但它们指向同一个堆对象。 |
**重点**:如果 `StringBuilder` 在方法内执行 `sb = new StringBuilder()`,外部的变量也不会变——因为改的是形参的引用指向,不是对象内容。
---
### Q3:一次性说清 final、finally、finalize() 的区别。final 修饰的 List 能不能调用 add()?
**考点**:三个关键词的精准区分
**答案**:
| 关键词 | 身份 | 作用时机 | 核心职责 |
| :--- | :--- | :--- | :--- |
| **final** | 关键字/修饰符 | 编译期 | 类不可继承、方法不可重写、**引用指向不可变** |
| **finally** | 异常处理关键字 | 运行期 | 无论是否异常,**保证资源释放**。唯一不执行的情况:`System.exit(0)` |
| **finalize()** | Object类的方法(JDK 9弃用) | GC运行期(不确定) | 对象被回收前的钩子。因时机不可控、性能差,已被弃用 |
**final 修饰的 List 能不能 add()?**
✅ **能!**
```java
final List<String> list = new ArrayList<>();
list.add("hello"); // ✅ 编译通过!修改的是堆中对象的内部状态
list.add("world"); // ✅ 编译通过!
list = new ArrayList<>(); // ❌ 编译报错!引用指向不可变
```
**原理**:`final` 锁死的是栈中的**引用地址**,而不是堆中的对象内容。`add()` 操作是在堆内存中修改数组,没改变引用地址。
---
### Q4:抽象类和接口的本质区别是什么?Java 8 引入 default 方法后,界限是否模糊了?什么时候必须用抽象类?
**考点**:面向对象设计理念(Is-A vs Like-A)、Java 版本特性
**答案**:
**设计理念**:
- **抽象类**:捕捉事物的**本质**(Is-A关系)——“它是什么”
- **接口**:定义事物的**能力/契约**(Like-A关系)——“它能做什么”
**核心区别**:
| 维度 | 抽象类 | 接口 |
| :--- | :--- | :--- |
| **状态(成员变量)** | ✅ 可以有 | ❌ 默认 `public static final`(无状态) |
| **构造方法** | ✅ 有 | ❌ 无 |
| **继承/实现** | 单继承 | 多实现 |
| **方法权限** | 支持 `protected`、`private` | 默认 `public` |
**Java 8 default 方法后界限模糊了吗?**
并没有!`default` 方法是接口演进的语法糖(方便给集合框架加新方法,不破坏旧代码)。核心区别在于**状态**——抽象类可以通过构造器维护属性,接口无法维护状态。只要涉及“属性/状态”的复用,必须用抽象类。
**实战场景选择**:
- 优先用**接口**(面向接口编程),因为 Java 单继承,用接口更灵活,且方便做动态代理。
- 不得已用**抽象类**:多个子类有大量重复代码和相同属性时(如 `BaseController` 统一做 Token 解析)。
---
### Q5:static 修饰的变量在 JVM 哪个区域(JDK 8)?为什么 Spring 的 @Autowired 不能注入到 static 字段?
**考点**:JVM 内存布局、Spring Bean 生命周期
**答案**:
**static 变量在 JDK 8 的存储位置**:**堆(Heap)**,具体在 `Klass` 对象(类的元数据)旁边。JDK 8 移除了永久代,静态变量不再存在方法区,而是跟随 Class 对象进入堆。
**为什么 `@Autowired` 不能注入 static 字段?**
Spring 依赖注入发生在**对象实例化阶段**(`Bean` 创建时),而 `static` 字段属于**类级别**,在类加载阶段就已初始化完成。Spring 容器无法干预类加载阶段,无法通过 `setter` 或字段反射去修改 `static` 字段(即使暴力反射也会受到 JVM 安全限制)。
**常见面试扩展**:如果确实需要将 Spring Bean 注入到静态工具类怎么办?可以用 `@PostConstruct` 在 Bean 初始化时手动赋值给静态变量。
---
## 第二板块:JVM 与内存模型
### Q6:JVM 哪些区域线程共享?哪些线程私有?栈上能不能存对象?
**考点**:JVM 运行时数据区
**答案**:
| 区域 | 线程共享? | 存储内容 | 异常 |
| :--- | :--- | :--- | :--- |
| **堆(Heap)** | ✅ 共享 | 所有对象实例、数组、字符串常量池(JDK8+) | OOM |
| **元空间(Metaspace)** | ✅ 共享 | 类元数据、静态变量、常量池 | OOM |
| **虚拟机栈(Stack)** | ❌ 私有 | 局部变量表、操作数栈、方法出口 | StackOverflowError |
| **本地方法栈** | ❌ 私有 | native 方法调用信息 | StackOverflowError |
| **程序计数器(PC)** | ❌ 私有 | 当前执行字节码的行号(书签) | 无 |
**栈上能不能存对象?**
能!通过**逃逸分析**,JVM 发现一个对象**不会逃逸出方法**(即不会作为返回值返回,也不会被其他线程访问),就可以在栈上分配对象,随栈帧弹出而自动销毁,减轻 GC 压力。这是 JIT 编译器的一个优化手段。
---
### Q7:GC 如何判定对象“已死”?GC Roots 包含什么?互相引用的对象会被回收吗?
**考点**:可达性分析算法
**答案**:
**判定方法**:**可达性分析算法**。从 `GC Roots` 出发,通过引用链(Reference Chain)向下搜索。若某对象到 `GC Roots` 没有任何引用链相连,则判定为可回收。
**GC Roots 包含**:
- 虚拟机栈(栈帧局部变量)引用的对象
- 静态变量(`static`)引用的对象
- 活跃的线程(`Thread`)
- JNI(`native` 方法)引用的对象
**互相引用的对象会被回收吗?**
✅ **会!** 两个对象互相引用,但没有任何 `GC Roots` 指向它们,可达性分析算法搜不到它们,GC 就会回收。这是可达性分析算法比引用计数法优秀的地方(引用计数法无法解决循环引用问题)。
---
### Q8:双亲委派模型是什么?Tomcat 为什么打破它?自己写 java.lang.String 能替换 JDK 的吗?
**考点**:类加载机制、安全隔离
**答案**:
**双亲委派模型**:当一个类加载器收到类加载请求,**优先委托给父加载器**。父加载器加载不了,才由自己加载。
**层级结构**:
```
启动类加载器(Bootstrap,C++编写,加载 rt.jar)
↑
扩展类加载器(Extension,加载 lib/ext)
↑
应用类加载器(AppClassLoader,加载 classpath)
↑
自定义类加载器
```
**为什么要双亲委派?** **安全**。防止核心 API(如 `java.lang.String`)被篡改。你的自定义类永远无法替换 JDK 核心类,因为加载请求会先委派给启动类加载器,它从 `rt.jar` 加载官方版本。
**Tomcat 为什么打破它?** 为了**应用隔离**。同一 Tomcat 部署多个 Web 应用,可能依赖不同版本的 `fastjson.jar`。Tomcat 采用**逆委派**:优先从 Web 应用的 `/WEB-INF/lib` 加载类,加载不到再向上委派。这样实现了应用级别的类隔离。
**自己写 `java.lang.String` 能替换 JDK 的吗?**
❌ **不能**。双亲委派模型保证核心类始终由启动类加载器加载。即使你写了一个 `java.lang.String`,加载请求会委派到启动类加载器,它从 `rt.jar` 加载官方版本,你的自定义类永远不会被加载。这也是 Java 安全性设计的核心体现。
---
### Q9:JMM 中的“主内存”和“工作内存”对应硬件什么?volatile 如何强制禁止缓存一致性?
**考点**:Java 内存模型(JMM)、硬件映射
**答案**:
| JMM 概念 | 硬件层级 |
| :--- | :--- |
| **主内存** | 物理内存(RAM) |
| **工作内存** | CPU 寄存器 + 多级高速缓存(L1/L2/L3 Cache) |
**为什么需要工作内存?** 因为 CPU 访问寄存器和缓存的速度比访问内存快 3-4 个数量级。
**volatile 如何保证可见性?**
1. **写操作**:将工作内存中修改后的值**立即刷新到主内存**,并通知其他 CPU 缓存该变量无效(通过**缓存一致性协议**,如 MESI 协议)。
2. **读操作**:**直接从主内存读取**,不读取 CPU 缓存中的过期值。
也就是说,`volatile` 强制将读写操作绕过 CPU 缓存,直接在主内存中操作,从而保证多线程间对该变量的可见性。
**扩展**:`volatile` 还通过**内存屏障**禁止指令重排序,保证有序性。
---
## 第三板块:并发编程与多线程
### Q10:Java 实现多线程有几种方式?run() 和 start() 区别?画一下线程状态流转图,BLOCKED 和 WAITING 的区别?
**考点**:线程创建、线程状态
**答案**:
**创建线程的 4 种方式**:
| 方式 | 说明 |
| :--- | :--- |
| 继承 `Thread` 类 | 重写 `run()`,用 `this` 表示当前线程 |
| 实现 `Runnable` 接口 | 更灵活,可用 Lambda |
| 实现 `Callable` 接口 | 有返回值,配合 `FutureTask` |
| 使用线程池 | `Executors` 或 `ThreadPoolExecutor` |
**run() vs start()**:
- `run()`:普通实例方法调用,**不创建新线程**,在当前线程执行
- `start()`:在操作系统**创建新线程**,自动调用 `run()`
**线程 6 种状态流转图**:
```
NEW --(start())--> RUNNABLE
RUNNABLE --(等待锁)--> BLOCKED --(获取锁)--> RUNNABLE
RUNNABLE --(wait()/join())--> WAITING --(notify()/notifyAll())--> RUNNABLE
RUNNABLE --(sleep(timeout)/wait(timeout))--> TIMED_WAITING --(超时/唤醒)--> RUNNABLE
RUNNABLE --(run() 结束)--> TERMINATED
```
**BLOCKED vs WAITING 的根本区别**:
| 状态 | 等待的目标 | 唤醒方式 | 是否释放锁 |
| :--- | :--- | :--- | :--- |
| **BLOCKED** | 等待**获取锁**(`synchronized` 竞争失败) | 锁被释放后自动唤醒 | 尚未持有锁,无需释放 |
| **WAITING** | 等待**其他线程通知**(`wait()` / `join()`) | 必须调用 `notify()`/`notifyAll()` | 持有锁,`wait()` 会**释放锁** |
---
### Q11:synchronized 在 JDK 1.6 之后做了哪些优化?描述锁升级过程。为什么轻量级锁用自旋?重量级锁为什么慢?
**考点**:synchronized 底层原理、锁优化、操作系统内核态
**答案**:
**JDK 1.6 优化**:引入了**锁升级**机制(不可逆),避免一上来就使用重量级锁。
**锁升级路径**:
```
无锁 → 偏向锁 → 轻量级锁(自旋锁) → 重量级锁
```
| 锁状态 | 原理 | 适用场景 |
| :--- | :--- | :--- |
| **偏向锁** | 在对象头中记录线程ID,该线程再次进入无需同步操作 | 锁几乎只有一个线程访问 |
| **轻量级锁** | CAS 自旋尝试获取锁,不阻塞线程 | 锁竞争不激烈,持有时间短 |
| **重量级锁** | 依赖操作系统 `mutex`,线程阻塞,进入等待队列 | 锁竞争激烈,持有时间长 |
**为什么轻量级锁用自旋?**
大部分情况下锁被持有时间很短,让线程**不放弃 CPU**,循环重试几次可能就拿到锁了。避免了阻塞和唤醒带来的用户态/内核态切换开销。
**重量级锁为什么慢?**
- 需要从**用户态切换到内核态**(系统调用)
- 线程会被挂起,等待操作系统调度唤醒
- 涉及上下文切换,开销极大(数百纳秒 vs 微秒级)
> **面试金句**:“synchronized 刚开始是乐观锁(偏向/轻量级),发现锁竞争激烈后膨胀为悲观锁(重量级),是一个**自适应的过程**。”
---
### Q12:什么是 CAS?如何保证原子性?ABA 问题是什么?如何解决?
**考点**:CAS 原理、原子类、ABA 问题
**答案**:
**CAS(Compare And Swap)**:一条 CPU 原子指令,完成 **“比较-交换”** 两个动作。
```java
// 伪代码(实际是 CPU 一条指令)
boolean CAS(address, expectedValue, newValue) {
if (*(address) == expectedValue) {
*(address) = newValue;
return true;
}
return false;
}
```
| 变量 | 含义 |
| :--- | :--- |
| V | 要修改的内存地址 |
| A | 旧的预期值 |
| B | 新的写入值 |
**如何保证原子性?** CPU 硬件层面将“比较”和“交换”封装为一条指令,不可被打断。Java 中的 `Unsafe.compareAndSwapInt()` 就是调用此硬件指令。
**ABA 问题**:变量从 A → B → A,CAS 认为没变,但实际上已经变了(相当于翻新机)。
**解决方案**:引入**版本号**。
```java
// AtomicStampedReference 带版本号的原子引用
AtomicStampedReference<String> ref = new AtomicStampedReference<>("A", 1);
ref.compareAndSet("A", "B", 1, 2); // 同时比较值和版本号
```
> 类比:手机是“新机”还是“翻新机”?商家不仅看外观,还看首次激活日期(版本号)。
---
### Q13:wait() 和 sleep() 的区别?为什么 wait() 必须在 synchronized 块中?notify 和 notifyAll 的区别?
**考点**:线程间通信
**答案**:
| 维度 | wait() | sleep() |
| :--- | :--- | :--- |
| 所属类 | Object | Thread |
| 是否需要 synchronized | **必须**(否则抛异常) | 不需要 |
| 是否释放锁 | ✅ **释放**(让其他线程可以进入同步块) | ❌ **不释放**(锁还拿着) |
| 唤醒方式 | 被 `notify()`/`notifyAll()` 唤醒 | 超时后自动唤醒 |
| 用途 | 线程间通信/协作 | 让线程暂停一段时间 |
**为什么 wait() 必须在 synchronized 块中?**
`wait()` 的目的是释放**当前持有的锁**,让其他线程进入同步块执行。如果不在同步块中,该线程根本没有持有锁,`wait()` 无锁可放,逻辑上无法成立。这也是 **“先持有锁,再释放锁”** 的基本逻辑要求。
**notify vs notifyAll**:
- `notify()`:唤醒**一个**等待线程(随机)
- `notifyAll()`:唤醒**所有**等待线程(它们重新竞争锁)
---
### Q14:ThreadPoolExecutor 核心参数?任务提交流程?CallerRunsPolicy 有什么风险?
**考点**:线程池原理、拒绝策略
**答案**:
**七大核心参数**:
```java
new ThreadPoolExecutor(
int corePoolSize, // 核心线程数(正式员工)
int maximumPoolSize, // 最大线程数(正式+临时工)
long keepAliveTime, // 临时工空闲存活时间
TimeUnit unit, // 时间单位
BlockingQueue<Runnable> workQueue, // 任务阻塞队列
ThreadFactory threadFactory, // 线程工厂
RejectedExecutionHandler handler // 拒绝策略
)
```
**任务提交流程**:
```
提交任务
↓
核心线程数未满? → 是 → 创建核心线程执行
↓ 否
工作队列未满? → 是 → 任务入队等待
↓ 否
最大线程数未满? → 是 → 创建临时线程执行
↓ 否
执行拒绝策略
```
**四种拒绝策略**:
| 策略 | 行为 | 风险 |
| :--- | :--- | :--- |
| `AbortPolicy`(默认) | 直接抛 `RejectedExecutionException` | 业务中断 |
| `CallerRunsPolicy` | 谁提交的任务谁自己执行(调用者线程) | **调用者线程被阻塞**,可能拖垮整个系统 |
| `DiscardOldestPolicy` | 丢弃队列中最老的任务,重新提交 | 数据丢失 |
| `DiscardPolicy` | 静默丢弃新任务 | 数据丢失 |
**CallerRunsPolicy 的风险**:当线程池已经过载,拒绝策略让提交任务的线程(如 Tomcat 工作线程)去执行任务,会导致该线程被阻塞,无法处理其他请求,最终可能造成整个应用的响应超时。
---
### Q15:手写双重检查锁(DCL)单例模式。为什么 instance 一定要加 volatile?
**考点**:单例模式、指令重排序
**答案**:
```java
public class Singleton {
private static volatile Singleton instance = null; // volatile 是关键!
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) { // 第一次检查(无锁)
synchronized (Singleton.class) {
if (instance == null) { // 第二次检查(有锁)
instance = new Singleton();
}
}
}
return instance;
}
}
```
**为什么要加 volatile?——防止指令重排序导致拿到“半成品对象”**
`new Singleton()` 在 JVM 中分三步:
1. 分配内存空间
2. 初始化对象(构造方法)
3. 将引用指向内存地址
JVM 可能重排序为 **1 → 3 → 2**(先指向引用,再初始化)。如果线程 A 执行到第 3 步(对象引用已指向内存,但对象尚未初始化),此时线程 B 调用 `getInstance()`,发现 `instance != null`,直接返回一个**未初始化的半成品对象**,程序崩溃。
`volatile` 通过**内存屏障**禁止指令重排序,保证**对象完全初始化后**引用才对其他线程可见。
---
## 第四板块:IO 流与网络编程
### Q16:BIO 和 NIO 的区别?Tomcat 默认用什么 IO 模型?为什么能支撑高并发?
**考点**:IO 模型演进、Selector 多路复用
**答案**:
| 维度 | BIO(同步阻塞) | NIO(同步非阻塞) |
| :--- | :--- | :--- |
| 线程模型 | 一个连接一个线程 | 一个线程管理多个连接(Selector) |
| 阻塞方式 | `accept()`、`read()` 都阻塞 | `select()` 阻塞,但读写不阻塞 |
| 并发上限 | 受限于线程数(几千) | 受限于操作系统(几万+) |
| 适用场景 | 连接数少、请求简单 | 连接数多、高并发 |
**Tomcat 默认 IO 模型**:**NIO**(`NioEndpoint`),基于 Java NIO 的 Selector 多路复用器实现。
**为什么 NIO 能支撑高并发?**
**核心组件**:
- **Channel**:双向传输通道(类似 Socket)
- **Buffer**:数据缓冲区
- **Selector(多路复用器)**:一个线程轮询多个 Channel,检测哪些有读写事件就绪
一个线程可以管理成千上万个连接,大大减少了线程切换和内存开销。
---
### Q17:为什么 TCP 建立连接是三次握手,关闭是四次挥手?TIME_WAIT 过多的原因和解决方案?
**考点**:TCP 协议细节、状态迁移
**答案**:
**三次握手(建立连接)**:
```
客户端 → 服务端:SYN(我想连你)
服务端 → 客户端:SYN+ACK(好的,我也准备好了)
客户端 → 服务端:ACK(收到,开始传数据吧)
```
**为什么不是两次?** 防止**过期连接请求**突然到达。若客户端第一次发的 SYN 因网络滞留,后来超时重发建立了连接。旧 SYN 突然到达,服务端以为是新连接就回复 SYN+ACK。如果是两次握手,服务端发完 SYN+ACK 就认为连接建立成功,会一直等待不存在的客户端数据。三次握手中,服务端要等客户端的 ACK 确认,才确定连接有效。
**四次挥手(关闭连接)**:
TCP 是全双工的,双方各自关闭各自的发送通道,所以需要四次。
| 步骤 | 动作 | 状态变化 |
| :--- | :--- | :--- |
| 1 | 客户端 → FIN | 客户端:FIN_WAIT_1 |
| 2 | 服务端 → ACK | 服务端:CLOSE_WAIT,客户端:FIN_WAIT_2 |
| 3 | 服务端 → FIN | 服务端:LAST_ACK |
| 4 | 客户端 → ACK | 客户端:TIME_WAIT(2MSL后关闭) |
**TIME_WAIT 过多的原因**:
- 服务端作为客户端(如微服务间 RPC 调用)频繁发起短连接
- 每次请求都新建 Socket,用完就关
- 导致大量连接处于 `TIME_WAIT` 状态,**耗尽本地端口号(65535 个)**
**解决方案**:
1. **使用 HTTP 连接池**(如 `PoolingHttpClientConnectionManager`),复用长连接
2. 开启 `SO_REUSEADDR`,允许重用地址
3. 缩短服务端超时时间
---
### Q18:GET 和 POST 的区别?301/302/304/401/403/503 状态码的含义?
**考点**:HTTP 协议
**答案**:
**GET vs POST 对比**:
| 维度 | GET | POST |
| :--- | :--- | :--- |
| **语义** | 获取数据 | 提交数据 |
| **参数位置** | Query String(URL 中) | Body(请求体中) |
| **幂等性** | ✅ 幂等(多次请求结果相同) | ❌ 非幂等 |
| **缓存** | 可被缓存 | 不可缓存 |
| **历史记录** | 保留在浏览器历史 | 不保留 |
**常见误区纠正**:
- ❌ “POST 比 GET 安全”——安全取决于是否用 HTTPS/加密,与方法无关
- ❌ “GET 有长度限制”——标准没有限制 URL 长度
- ❌ “GET 只能传文本”——可通过 `urlencode` 传二进制数据
**状态码大全**:
| 状态码 | 含义 | 场景 |
| :--- | :--- | :--- |
| **301** | 永久重定向 | 域名迁移,HTTP → HTTPS |
| **302** | 临时重定向 | 登录成功跳转首页 |
| **304** | 缓存未过期 | 浏览器使用本地缓存 |
| **401** | 未认证 | 未登录(没传 Token) |
| **403** | 已认证但无权限 | 普通用户访问管理员接口 |
| **503** | 服务不可用 | 服务器过载或宕机 |
---
### Q19:HTTPS 如何保证安全传输?对称加密和非对称加密在握手过程中分别起什么作用?CA 证书是什么?中间人攻击为什么被防住?
**考点**:HTTPS 加密流程、证书验证
**答案**:
**HTTPS = HTTP + 加密层(TLS/SSL)**
**加密流程中三组密钥**:
| 密钥组 | 类型 | 持有方 | 作用 |
| :--- | :--- | :--- | :--- |
| 第一组 | 非对称加密 | CA 机构(公钥在浏览器/操作系统中) | **校验证书**是否被篡改 |
| 第二组 | 非对称加密 | 服务器生成(公钥放在证书中发给客户端) | **协商传输用的对称密钥** |
| 第三组 | 对称加密 | 客户端生成,传给服务器 | **实际数据传输**(速度更快) |
**完整握手流程**:
```
1. 服务器向 CA 机构申请证书(证书 = 域名信息 + 服务器公钥 + CA 数字签名)
2. 浏览器访问 → 服务器返回证书
3. 浏览器用操作系统内置的 CA 公钥验证证书签名(防篡改)
4. 验证通过后,浏览器生成对称密钥,用服务器公钥加密后发送
5. 服务器用私钥解密,得到对称密钥
6. 后续通信全部用对称加密
```
**中间人攻击为什么被防住?**
中间人若篡改证书内容,但没有 CA 私钥重新生成签名,浏览器验证签名会失败,立即终止通信。若中间人想整体掉包成自己申请的证书,但证书中包含**域名信息**,浏览器会识别出域名与请求地址不一致,也会发出警告并终止连接。
---
## 第五板块:数据库与 Spring
### Q20:MySQL 为什么用 B+ 树索引而不是 B 树或红黑树?什么是回表和覆盖索引?如何用 EXPLAIN 判断索引是否生效?
**考点**:索引底层数据结构、SQL 优化
**答案**:
**为什么是 B+ 树?**
| 数据结构 | 高度 | 磁盘 IO 次数 | 范围查询 | MySQL 选择 |
| :--- | :--- | :--- | :--- | :--- |
| 红黑树 | 高(几十层) | 多 | 需要中序遍历 | ❌ |
| B 树 | 低(3-4 层) | 少 | 需要中序遍历 | ❌ |
| **B+ 树** | **低(3-4 层)** | **少** | **叶子节点双向链表** | ✅ |
**B+ 树核心特点**:
1. 非叶子节点只存**索引键 + 指针**(不存数据)→ 一页放更多索引
2. 所有数据在**叶子节点** → 查询稳定(都要到叶子)
3. 叶子节点形成**有序双向链表** → 范围查询极快(`BETWEEN`、`>`、`<`)
**回表 vs 覆盖索引**:
- **聚簇索引(主键)**:叶子节点存**完整行数据**
- **二级索引(非主键)**:叶子节点存**主键值**
- **回表**:先查二级索引得主键,再回聚簇索引查完整行(两次查询)
- **覆盖索引**:查询的字段在二级索引树上已经包含,**不需要回表**(如 `SELECT id FROM user WHERE name='张三'`,name 有索引,id 是主键)
**EXPLAIN 关键列**:
| 列 | 关注点 |
| :--- | :--- |
| **type** | 访问类型,从优到差:`system > const > eq_ref > ref > range > index > ALL`。至少达到 `range`,最好 `ref`。`ALL` 是全表扫描。 |
| **possible_keys** | 可能用的索引 |
| **key** | 实际用的索引 |
| **rows** | 预估扫描行数 |
| **Extra** | `Using index`(覆盖索引,最优)、`Using filesort`(需要额外排序,应避免) |
---
### Q21:MySQL 默认隔离级别是什么?如何解决幻读?(MVCC + 间隙锁)
**考点**:事务隔离级别、MVCC、间隙锁
**答案**:
**MySQL InnoDB 默认隔离级别**:**可重复读(Repeatable Read,RR)**
**并发事务带来的三个问题**:
| 问题 | 表现 |
| :--- | :--- |
| **脏读** | 读到另一个事务**未提交**的数据(回滚后就没了) |
| **不可重复读** | 同一事务内两次读同一条记录,结果不同(其他事务修改了) |
| **幻读** | 同一事务内两次查询结果集条数不同(其他事务插入了新行) |
**MySQL RR 级别如何解决幻读?**
**① MVCC(多版本并发控制)**
每行记录有三个隐藏字段:`DB_TRX_ID`(最近修改的事务ID)、`DB_ROLL_PTR`(回滚指针)、`DB_ROW_ID`(行ID)。
RR 级别下,事务的 `ReadView` **只在第一次查询时生成**,后续查询复用同一个 `ReadView`。快照读(`SELECT`)读取的是事务开始时的快照版本,不受其他事务插入影响。
**② 间隙锁(Gap Lock)**
当前读(`SELECT ... FOR UPDATE`、`UPDATE`、`DELETE`)时,不仅锁定匹配的行,还会锁定查询范围内的**间隙**,阻止其他事务在范围内插入新行,从而解决幻读。
---
### Q22:@Transactional 在什么情况下会失效?(至少 4 种)
**考点**:Spring 事务原理、AOP 代理
**答案**:
| 失效场景 | 原因 | 解决方案 |
| :--- | :--- | :--- |
| **① 方法不是 public** | Spring 事务代理只对 `public` 方法生效 | 改为 `public` |
| **② 同类自调用** | `this.method()` 绕过代理对象,事务切面不执行 | 注入自身代理对象 `self.method()` |
| **③ 异常被 catch 吞掉** | 事务只对未捕获的 `RuntimeException` 回滚 | 重新抛出或在 catch 中手动回滚 |
| **④ 数据库引擎不支持事务** | MySQL MyISAM 引擎不支持事务 | 使用 InnoDB |
| **⑤ 事务传播行为配置错误** | `@Transactional(propagation = Propagation.NOT_SUPPORTED)` 会强行挂起事务 | 检查传播行为配置 |
| **⑥ 未配置事务管理器** | 没有 `PlatformTransactionManager` Bean | 添加 `@EnableTransactionManagement` |
**自调用解决方案**:
```java
@Service
public class UserService {
@Autowired
private UserService self; // 注入自身代理对象
public void method1() {
self.method2(); // 通过代理调用,事务生效
}
@Transactional
public void method2() { ... }
}
```
---
### Q23:Spring 如何通过三级缓存解决循环依赖?为什么构造器注入不支持而 Setter 注入支持?
**考点**:Spring IoC 容器、循环依赖解决原理
**答案**:
**三级缓存**:
| 缓存 | 名称 | 存储内容 |
| :--- | :--- | :--- |
| **一级缓存** | `singletonObjects` | 完全初始化好的 Bean(成品) |
| **二级缓存** | `earlySingletonObjects` | 提前暴露的半成品 Bean(未完成属性注入) |
| **三级缓存** | `singletonFactories` | `ObjectFactory` 工厂对象(用于生成代理对象) |
**核心流程(A 依赖 B,B 依赖 A)**:
```
1. 创建 A 实例(无参构造)→ 将 A 的 ObjectFactory 放入三级缓存
2. 填充 A 属性,发现需要 B
3. 创建 B 实例 → 将 B 的 ObjectFactory 放入三级缓存
4. 填充 B 属性,发现需要 A
5. 从三级缓存获取 A 的 ObjectFactory → 生成 A 的早期引用 → 放入二级缓存
6. B 完成属性注入和初始化 → 放入一级缓存
7. 回到 A,从一级缓存获取 B → A 完成注入和初始化 → 放入一级缓存
```
**为什么需要三级缓存而不是二级?**
如果 Bean 被 AOP 代理,需要在早期阶段就生成代理对象(而非普通对象)。三级缓存存放工厂对象,可以在**需要代理时才生成代理对象**,而不是提前创建。这是为了处理 **AOP 与循环依赖共存** 的场景。
**为什么构造器注入不支持循环依赖?**
构造器注入在**创建实例阶段**就需要依赖其他 Bean。A 要通过构造器创建,需要 B 的实例;B 要通过构造器创建,需要 A 的实例。形成死锁,Spring 无法解决。
Setter 注入(或 Field 注入)允许**先通过无参构造创建对象**(此时是半成品),再通过 Setter 注入依赖,所以三级缓存可以生效。
## 📝 面试速查表
| 板块 | 高频考点 | 你的复习状态 |
| :--- | :--- | :--- |
| Java 基础 | `==` vs `equals`、引用传递、`final/ finally/ finalize()` | ✅ |
| JVM | 内存结构、GC、类加载、JMM | ✅ |
| 并发编程 | 锁升级、CAS、线程池、单例 DCL、`wait/notify` | ✅ |
| IO/网络 | BIO/NIO、TCP 状态、HTTP/HTTPS | ✅ |
| 数据库/Spring | B+ 树、MVCC、事务失效、循环依赖 | ✅ |
---
祝面试顺利!有哪道题觉得思路卡壳,随时告诉我,我帮你再深挖底层源码。💪😊