
这份面试题清单完全是**根据你这一个月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**什么是 CASCompare 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_REUSEADDR18. **HTTP 协议**请指出 GET 和 POST 请求在语义、参数传递Body/QueryString、幂等性上的区别。常见的 HTTP 状态码 301、302、304、401、403、503 分别代表什么19. **HTTPS 加密**HTTPS 是如何保证安全传输的解释一下对称加密和非对称加密在 HTTPS 握手过程中分别起什么作用什么是 CA 证书中间人攻击为什么被防住了---### 第五板块数据库与 SpringJ2EE 核心*对应MySQL 索引/事务、Spring IoC/AOP/事务*20. **索引原理**MySQL 为什么使用 B 树作为索引结构而不是 B 树或红黑树什么是“回表”和“覆盖索引”如何通过 SQL 语句判断查询是否走了索引EXPLAIN 看 type 和 Extra21. **事务隔离**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### Q1int 和 Integer 在内存存储上有什么本质区别为什么阿里巴巴开发手册强制要求包装类对象值比较要用 equals不能用 **考点**基本类型 vs 引用类型、包装类缓存池**答案**| 维度 | int基本类型 | Integer引用类型/包装类 || :--- | :--- | :--- || 存储位置 | 栈中直接存储**数值** | 栈中存储**引用地址**指向堆中的对象 || 初始值 | 0 | null || 内存占用 | 4字节 | 对象头实例数据约16字节 || 可参与泛型 | ❌ | ✅ |**为什么不能用 比较 Integer** 对引用类型比较的是**栈中的地址值**而非堆中的数值。javaInteger 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---### Q2Java 只有值传递。为什么 String 作为参数传入方法后修改外部不变而 StringBuilder 传入后 append 外部会变**考点**值传递的本质栈中副本地址String 不可变性**答案**Java 确实**只有值传递**。所谓值传递就是实参将自己的**副本**交给形参。javapublic 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()**✅ **能**javafinal ListString 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 解析。---### Q5static 修饰的变量在 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 与内存模型### Q6JVM 哪些区域线程共享哪些线程私有栈上能不能存对象**考点**JVM 运行时数据区**答案**| 区域 | 线程共享 | 存储内容 | 异常 || :--- | :--- | :--- | :--- || **堆Heap** | ✅ 共享 | 所有对象实例、数组、字符串常量池JDK8 | OOM || **元空间Metaspace** | ✅ 共享 | 类元数据、静态变量、常量池 | OOM || **虚拟机栈Stack** | ❌ 私有 | 局部变量表、操作数栈、方法出口 | StackOverflowError || **本地方法栈** | ❌ 私有 | native 方法调用信息 | StackOverflowError || **程序计数器PC** | ❌ 私有 | 当前执行字节码的行号书签 | 无 |**栈上能不能存对象**能通过**逃逸分析**JVM 发现一个对象**不会逃逸出方法**即不会作为返回值返回也不会被其他线程访问就可以在栈上分配对象随栈帧弹出而自动销毁减轻 GC 压力。这是 JIT 编译器的一个优化手段。---### Q7GC 如何判定对象“已死”GC Roots 包含什么互相引用的对象会被回收吗**考点**可达性分析算法**答案****判定方法****可达性分析算法**。从 GC Roots 出发通过引用链Reference Chain向下搜索。若某对象到 GC Roots 没有任何引用链相连则判定为可回收。**GC Roots 包含**- 虚拟机栈栈帧局部变量引用的对象- 静态变量static引用的对象- 活跃的线程Thread- JNInative 方法引用的对象**互相引用的对象会被回收吗**✅ **会** 两个对象互相引用但没有任何 GC Roots 指向它们可达性分析算法搜不到它们GC 就会回收。这是可达性分析算法比引用计数法优秀的地方引用计数法无法解决循环引用问题。---### Q8双亲委派模型是什么Tomcat 为什么打破它自己写 java.lang.String 能替换 JDK 的吗**考点**类加载机制、安全隔离**答案****双亲委派模型**当一个类加载器收到类加载请求**优先委托给父加载器**。父加载器加载不了才由自己加载。**层级结构**启动类加载器BootstrapC编写加载 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 安全性设计的核心体现。---### Q9JMM 中的“主内存”和“工作内存”对应硬件什么volatile 如何强制禁止缓存一致性**考点**Java 内存模型JMM、硬件映射**答案**| JMM 概念 | 硬件层级 || :--- | :--- || **主内存** | 物理内存RAM || **工作内存** | CPU 寄存器 多级高速缓存L1/L2/L3 Cache |**为什么需要工作内存** 因为 CPU 访问寄存器和缓存的速度比访问内存快 3-4 个数量级。**volatile 如何保证可见性**1. **写操作**将工作内存中修改后的值**立即刷新到主内存**并通知其他 CPU 缓存该变量无效通过**缓存一致性协议**如 MESI 协议。2. **读操作****直接从主内存读取**不读取 CPU 缓存中的过期值。也就是说volatile 强制将读写操作绕过 CPU 缓存直接在主内存中操作从而保证多线程间对该变量的可见性。**扩展**volatile 还通过**内存屏障**禁止指令重排序保证有序性。---## 第三板块并发编程与多线程### Q10Java 实现多线程有几种方式run() 和 start() 区别画一下线程状态流转图BLOCKED 和 WAITING 的区别**考点**线程创建、线程状态**答案****创建线程的 4 种方式**| 方式 | 说明 || :--- | :--- || 继承 Thread 类 | 重写 run()用 this 表示当前线程 || 实现 Runnable 接口 | 更灵活可用 Lambda || 实现 Callable 接口 | 有返回值配合 FutureTask || 使用线程池 | Executors 或 ThreadPoolExecutor |**run() vs start()**- run()普通实例方法调用**不创建新线程**在当前线程执行- start()在操作系统**创建新线程**自动调用 run()**线程 6 种状态流转图**NEW --(start())-- RUNNABLERUNNABLE --(等待锁)-- BLOCKED --(获取锁)-- RUNNABLERUNNABLE --(wait()/join())-- WAITING --(notify()/notifyAll())-- RUNNABLERUNNABLE --(sleep(timeout)/wait(timeout))-- TIMED_WAITING --(超时/唤醒)-- RUNNABLERUNNABLE --(run() 结束)-- TERMINATED**BLOCKED vs WAITING 的根本区别**| 状态 | 等待的目标 | 唤醒方式 | 是否释放锁 || :--- | :--- | :--- | :--- || **BLOCKED** | 等待**获取锁**synchronized 竞争失败 | 锁被释放后自动唤醒 | 尚未持有锁无需释放 || **WAITING** | 等待**其他线程通知**wait() / join() | 必须调用 notify()/notifyAll() | 持有锁wait() 会**释放锁** |---### Q11synchronized 在 JDK 1.6 之后做了哪些优化描述锁升级过程。为什么轻量级锁用自旋重量级锁为什么慢**考点**synchronized 底层原理、锁优化、操作系统内核态**答案****JDK 1.6 优化**引入了**锁升级**机制不可逆避免一上来就使用重量级锁。**锁升级路径**无锁 → 偏向锁 → 轻量级锁自旋锁 → 重量级锁| 锁状态 | 原理 | 适用场景 || :--- | :--- | :--- || **偏向锁** | 在对象头中记录线程ID该线程再次进入无需同步操作 | 锁几乎只有一个线程访问 || **轻量级锁** | CAS 自旋尝试获取锁不阻塞线程 | 锁竞争不激烈持有时间短 || **重量级锁** | 依赖操作系统 mutex线程阻塞进入等待队列 | 锁竞争激烈持有时间长 |**为什么轻量级锁用自旋**大部分情况下锁被持有时间很短让线程**不放弃 CPU**循环重试几次可能就拿到锁了。避免了阻塞和唤醒带来的用户态/内核态切换开销。**重量级锁为什么慢**- 需要从**用户态切换到内核态**系统调用- 线程会被挂起等待操作系统调度唤醒- 涉及上下文切换开销极大数百纳秒 vs 微秒级 **面试金句**“synchronized 刚开始是乐观锁偏向/轻量级发现锁竞争激烈后膨胀为悲观锁重量级是一个**自适应的过程**。”---### Q12什么是 CAS如何保证原子性ABA 问题是什么如何解决**考点**CAS 原理、原子类、ABA 问题**答案****CASCompare 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 → ACAS 认为没变但实际上已经变了相当于翻新机。**解决方案**引入**版本号**。java// AtomicStampedReference 带版本号的原子引用AtomicStampedReferenceString ref new AtomicStampedReference(A, 1);ref.compareAndSet(A, B, 1, 2); // 同时比较值和版本号 类比手机是“新机”还是“翻新机”商家不仅看外观还看首次激活日期版本号。---### Q13wait() 和 sleep() 的区别为什么 wait() 必须在 synchronized 块中notify 和 notifyAll 的区别**考点**线程间通信**答案**| 维度 | wait() | sleep() || :--- | :--- | :--- || 所属类 | Object | Thread || 是否需要 synchronized | **必须**否则抛异常 | 不需要 || 是否释放锁 | ✅ **释放**让其他线程可以进入同步块 | ❌ **不释放**锁还拿着 || 唤醒方式 | 被 notify()/notifyAll() 唤醒 | 超时后自动唤醒 || 用途 | 线程间通信/协作 | 让线程暂停一段时间 |**为什么 wait() 必须在 synchronized 块中**wait() 的目的是释放**当前持有的锁**让其他线程进入同步块执行。如果不在同步块中该线程根本没有持有锁wait() 无锁可放逻辑上无法成立。这也是 **“先持有锁再释放锁”** 的基本逻辑要求。**notify vs notifyAll**- notify()唤醒**一个**等待线程随机- notifyAll()唤醒**所有**等待线程它们重新竞争锁---### Q14ThreadPoolExecutor 核心参数任务提交流程CallerRunsPolicy 有什么风险**考点**线程池原理、拒绝策略**答案****七大核心参数**javanew ThreadPoolExecutor(int corePoolSize, // 核心线程数正式员工int maximumPoolSize, // 最大线程数正式临时工long keepAliveTime, // 临时工空闲存活时间TimeUnit unit, // 时间单位BlockingQueueRunnable workQueue, // 任务阻塞队列ThreadFactory threadFactory, // 线程工厂RejectedExecutionHandler handler // 拒绝策略)**任务提交流程**提交任务↓核心线程数未满 → 是 → 创建核心线程执行↓ 否工作队列未满 → 是 → 任务入队等待↓ 否最大线程数未满 → 是 → 创建临时线程执行↓ 否执行拒绝策略**四种拒绝策略**| 策略 | 行为 | 风险 || :--- | :--- | :--- || AbortPolicy默认 | 直接抛 RejectedExecutionException | 业务中断 || CallerRunsPolicy | 谁提交的任务谁自己执行调用者线程 | **调用者线程被阻塞**可能拖垮整个系统 || DiscardOldestPolicy | 丢弃队列中最老的任务重新提交 | 数据丢失 || DiscardPolicy | 静默丢弃新任务 | 数据丢失 |**CallerRunsPolicy 的风险**当线程池已经过载拒绝策略让提交任务的线程如 Tomcat 工作线程去执行任务会导致该线程被阻塞无法处理其他请求最终可能造成整个应用的响应超时。---### Q15手写双重检查锁DCL单例模式。为什么 instance 一定要加 volatile**考点**单例模式、指令重排序**答案**javapublic 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 流与网络编程### Q16BIO 和 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我想连你服务端 → 客户端SYNACK好的我也准备好了客户端 → 服务端ACK收到开始传数据吧**为什么不是两次** 防止**过期连接请求**突然到达。若客户端第一次发的 SYN 因网络滞留后来超时重发建立了连接。旧 SYN 突然到达服务端以为是新连接就回复 SYNACK。如果是两次握手服务端发完 SYNACK 就认为连接建立成功会一直等待不存在的客户端数据。三次握手中服务端要等客户端的 ACK 确认才确定连接有效。**四次挥手关闭连接**TCP 是全双工的双方各自关闭各自的发送通道所以需要四次。| 步骤 | 动作 | 状态变化 || :--- | :--- | :--- || 1 | 客户端 → FIN | 客户端FIN_WAIT_1 || 2 | 服务端 → ACK | 服务端CLOSE_WAIT客户端FIN_WAIT_2 || 3 | 服务端 → FIN | 服务端LAST_ACK || 4 | 客户端 → ACK | 客户端TIME_WAIT2MSL后关闭 |**TIME_WAIT 过多的原因**- 服务端作为客户端如微服务间 RPC 调用频繁发起短连接- 每次请求都新建 Socket用完就关- 导致大量连接处于 TIME_WAIT 状态**耗尽本地端口号65535 个****解决方案**1. **使用 HTTP 连接池**如 PoolingHttpClientConnectionManager复用长连接2. 开启 SO_REUSEADDR允许重用地址3. 缩短服务端超时时间---### Q18GET 和 POST 的区别301/302/304/401/403/503 状态码的含义**考点**HTTP 协议**答案****GET vs POST 对比**| 维度 | GET | POST || :--- | :--- | :--- || **语义** | 获取数据 | 提交数据 || **参数位置** | Query StringURL 中 | Body请求体中 || **幂等性** | ✅ 幂等多次请求结果相同 | ❌ 非幂等 || **缓存** | 可被缓存 | 不可缓存 || **历史记录** | 保留在浏览器历史 | 不保留 |**常见误区纠正**- ❌ “POST 比 GET 安全”——安全取决于是否用 HTTPS/加密与方法无关- ❌ “GET 有长度限制”——标准没有限制 URL 长度- ❌ “GET 只能传文本”——可通过 urlencode 传二进制数据**状态码大全**| 状态码 | 含义 | 场景 || :--- | :--- | :--- || **301** | 永久重定向 | 域名迁移HTTP → HTTPS || **302** | 临时重定向 | 登录成功跳转首页 || **304** | 缓存未过期 | 浏览器使用本地缓存 || **401** | 未认证 | 未登录没传 Token || **403** | 已认证但无权限 | 普通用户访问管理员接口 || **503** | 服务不可用 | 服务器过载或宕机 |---### Q19HTTPS 如何保证安全传输对称加密和非对称加密在握手过程中分别起什么作用CA 证书是什么中间人攻击为什么被防住**考点**HTTPS 加密流程、证书验证**答案****HTTPS HTTP 加密层TLS/SSL****加密流程中三组密钥**| 密钥组 | 类型 | 持有方 | 作用 || :--- | :--- | :--- | :--- || 第一组 | 非对称加密 | CA 机构公钥在浏览器/操作系统中 | **校验证书**是否被篡改 || 第二组 | 非对称加密 | 服务器生成公钥放在证书中发给客户端 | **协商传输用的对称密钥** || 第三组 | 对称加密 | 客户端生成传给服务器 | **实际数据传输**速度更快 |**完整握手流程**1. 服务器向 CA 机构申请证书证书 域名信息 服务器公钥 CA 数字签名2. 浏览器访问 → 服务器返回证书3. 浏览器用操作系统内置的 CA 公钥验证证书签名防篡改4. 验证通过后浏览器生成对称密钥用服务器公钥加密后发送5. 服务器用私钥解密得到对称密钥6. 后续通信全部用对称加密**中间人攻击为什么被防住**中间人若篡改证书内容但没有 CA 私钥重新生成签名浏览器验证签名会失败立即终止通信。若中间人想整体掉包成自己申请的证书但证书中包含**域名信息**浏览器会识别出域名与请求地址不一致也会发出警告并终止连接。---## 第五板块数据库与 Spring### Q20MySQL 为什么用 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需要额外排序应避免 |---### Q21MySQL 默认隔离级别是什么如何解决幻读MVCC 间隙锁**考点**事务隔离级别、MVCC、间隙锁**答案****MySQL InnoDB 默认隔离级别****可重复读Repeatable ReadRR****并发事务带来的三个问题**| 问题 | 表现 || :--- | :--- || **脏读** | 读到另一个事务**未提交**的数据回滚后就没了 || **不可重复读** | 同一事务内两次读同一条记录结果不同其他事务修改了 || **幻读** | 同一事务内两次查询结果集条数不同其他事务插入了新行 |**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时不仅锁定匹配的行还会锁定查询范围内的**间隙**阻止其他事务在范围内插入新行从而解决幻读。---### Q22Transactional 在什么情况下会失效至少 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 |**自调用解决方案**javaServicepublic class UserService {Autowiredprivate UserService self; // 注入自身代理对象public void method1() {self.method2(); // 通过代理调用事务生效}Transactionalpublic void method2() { ... }}---### Q23Spring 如何通过三级缓存解决循环依赖为什么构造器注入不支持而 Setter 注入支持**考点**Spring IoC 容器、循环依赖解决原理**答案****三级缓存**| 缓存 | 名称 | 存储内容 || :--- | :--- | :--- || **一级缓存** | singletonObjects | 完全初始化好的 Bean成品 || **二级缓存** | earlySingletonObjects | 提前暴露的半成品 Bean未完成属性注入 || **三级缓存** | singletonFactories | ObjectFactory 工厂对象用于生成代理对象 |**核心流程A 依赖 BB 依赖 A**1. 创建 A 实例无参构造→ 将 A 的 ObjectFactory 放入三级缓存2. 填充 A 属性发现需要 B3. 创建 B 实例 → 将 B 的 ObjectFactory 放入三级缓存4. 填充 B 属性发现需要 A5. 从三级缓存获取 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、事务失效、循环依赖 | ✅ |---祝面试顺利有哪道题觉得思路卡壳随时告诉我我帮你再深挖底层源码。