跳转至

Java 面试题

40 道 Java 面试高频题 + 详细解答,覆盖 JVM 、集合框架、并发编程、 Spring 等核心知识点。


一、 JVM ( 12 题)

1. JVM 内存结构是怎样的?各区域的作用分别是什么

JVM 运行时数据区:

Text Only
┌─────────────────────────────────────────────┐
│                   JVM内存                     │
│                                              │
│  ┌─────────────────────────────────────┐    │
│  │         线程共享区域                   │    │
│  │  ┌────────────┐  ┌──────────────┐   │    │
│  │  │   堆(Heap)  │  │  方法区/元空间  │   │    │
│  │  │  对象实例    │  │  类信息/常量池  │   │    │
│  │  │  数组       │  │  静态变量      │   │    │
│  │  └────────────┘  └──────────────┘   │    │
│  └─────────────────────────────────────┘    │
│                                              │
│  ┌─────────────────────────────────────┐    │
│  │         线程私有区域                   │    │
│  │  ┌──────┐ ┌────────┐ ┌──────────┐  │    │
│  │  │虚拟机栈│ │程序计数器 │ │本地方法栈  │  │    │
│  │  │局部变量│ │当前指令  │ │  Native  │  │    │
│  │  │操作数栈│ │  地址   │ │  方法调用 │  │    │
│  │  └──────┘ └────────┘ └──────────┘  │    │
│  └─────────────────────────────────────┘    │
└─────────────────────────────────────────────┘

各区域详解:

1. 堆( Heap )— 线程共享 - 存储对象实例数组 - JVM 最大的一块内存区域 - GC 的主要工作区域 - 分为年轻代( Young )和老年代( Old )

Text Only
堆内存:
├── 年轻代(Young Generation) — 新创建的对象
│   ├── Eden区 (80%)         — 对象最初分配
│   ├── Survivor From (10%)  — 存活对象
│   └── Survivor To (10%)    — GC复制目标
└── 老年代(Old Generation)   — 长期存活的对象

2. 方法区( Method Area )/ 元空间( Metaspace )— 线程共享 - 存储类的元信息(类名、方法、字段) - 运行时常量池 - 静态变量 - JDK 8 之前是永久代( PermGen ), JDK 8+改为元空间( Metaspace ),使用本地内存

3. 虚拟机栈( VM Stack )— 线程私有 - 每个方法调用创建一个栈帧( Stack Frame ) - 栈帧包含: - 局部变量表(基本类型、对象引用) - 操作数栈 - 动态链接 - 方法返回地址 - 栈深度超限 → StackOverflowError - 无法扩展 → OutOfMemoryError

4. 程序计数器( PC Register )— 线程私有 - 记录当前线程执行的字节码指令地址 - 线程切换时需要恢复到正确位置 - 唯一一个不会发生 OOM 的区域

5. 本地方法栈( Native Method Stack )— 线程私有 - 为 Native 方法( C/C++实现)服务 - 功能类似虚拟机栈

2. 对象创建的过程是什么?对象在内存中的布局

对象创建过程:

Text Only
new Object()
1. 类加载检查: 检查类是否已加载
    ↓ (如未加载则先加载类)
2. 分配内存: 在堆中分配对象所需内存
   - 指针碰撞(Bump the Pointer): 堆内存规整时
   - 空闲列表(Free List): 堆内存不规整时
   - TLAB(Thread Local Allocation Buffer): 线程本地缓冲区, 避免并发
3. 内存初始化为零值: int→0, boolean→false, 引用→null
4. 设置对象头: 类指针、哈希码、GC年龄、锁标志
5. 执行<init>: 调用构造方法初始化

对象的内存布局:

Text Only
┌──────────────────────────────┐
│         对象头(Header)        │
│  ├── Mark Word (8字节/64位)   │  哈希码、GC年龄、锁标志、线程ID
│  ├── 类型指针 (4/8字节)       │  指向类的元数据
│  └── 数组长度 (4字节, 仅数组)  │
├──────────────────────────────┤
│       实例数据(Instance Data)  │  对象的字段数据
├──────────────────────────────┤
│       对齐填充(Padding)       │  补齐为8字节的整数倍
└──────────────────────────────┘

Mark Word ( 64 位 JVM ):

锁状态 存储内容 标志位
无锁 hashCode + age + 偏向锁标志(0) 01
偏向锁 threadID + age + 偏向锁标志(1) 01
轻量级锁 指向栈中锁记录的指针 00
重量级锁 指向 Monitor 的指针 10
GC 标记 11

3. 垃圾回收算法有哪些

1. 标记-清除( Mark-Sweep )

Text Only
标记阶段: 从GC Roots遍历,标记所有可达(存活)对象
清除阶段: 回收所有未标记(不可达)的对象

优点: 简单
缺点: 产生内存碎片,分配效率低

2. 复制算法( Copying )

Text Only
将内存分为两块(From和To)
只使用From区分配对象
GC时: 将From中存活对象复制到To区,然后清空From
交换From和To的角色

优点: 无碎片,分配效率高
缺点: 内存利用率只有50%
适用: 年轻代(大部分对象朝生夕死, 存活率低, 复制开销小)

3. 标记-整理( Mark-Compact )

Text Only
标记阶段: 标记所有存活对象
整理阶段: 将存活对象向一端移动,清理端边界以外的内存

优点: 无碎片
缺点: 移动对象需要更新引用,效率相对较低
适用: 老年代(对象存活率高)

4. 分代收集( Generational Collection )

Text Only
年轻代: 复制算法 (80%的对象在第一次GC就被回收)
   - Eden区满时触发Minor GC
   - 存活对象复制到Survivor区, 年龄+1
   - 年龄达到阈值(默认15)晋升到老年代

老年代: 标记-清除 或 标记-整理
   - 空间不足时触发Major GC / Full GC

GC Roots 有哪些? - 虚拟机栈中引用的对象 - 方法区中静态变量引用的对象 - 方法区中常量引用的对象 - 本地方法栈中 Native 方法引用的对象 - 同步锁( synchronized )持有的对象

4. 常见的 GC 收集器有哪些?各自的特点

收集器 作用区域 算法 特点 适用场景
Serial 年轻代 复制 单线程, STW 客户端/小堆
Serial Old 老年代 标记-整理 单线程, STW 客户端/小堆
ParNew 年轻代 复制 多线程, STW 配合 CMS
Parallel Scavenge 年轻代 复制 多线程,吞吐量优先 后台计算
Parallel Old 老年代 标记-整理 多线程 配合 Parallel Scavenge
CMS 老年代 标记-清除 低停顿,并发收集 Web 服务
G1 全堆 分区复制+整理 低停顿,可预测 大堆( 6GB+)
ZGC 全堆 并发整理 超低停顿(<10ms) 超大堆

CMS 收集器四个阶段:

Text Only
1. 初始标记(STW)   : 标记GC Roots直接关联的对象 → 很快
2. 并发标记(并发)   : 从GC Roots遍历整个对象图 → 耗时但并发
3. 重新标记(STW)   : 修正并发标记期间产生变动的对象 → 较快
4. 并发清除(并发)   : 清除不可达对象 → 耗时但并发

CMS缺点: 产生碎片、浮动垃圾、CPU敏感

G1 收集器:

Text Only
- 将堆划分为多个等大的Region(1-32MB)
- Region可以是Eden, Survivor, Old, Humongous(大对象)
- 优先回收垃圾最多的Region (Garbage First)
- 可以设置停顿时间目标: -XX:MaxGCPauseMillis=200
- Mixed GC: 同时回收年轻代和部分老年代Region

G1四个阶段:
1. 初始标记(STW)
2. 并发标记
3. 最终标记(STW)
4. 筛选回收(STW, 选择回收价值高的Region)

ZGC ( JDK 11+): - 停顿时间不超过 10ms (不随堆大小增长) - 支持 TB 级别的堆 - 使用着色指针( Colored Pointer )和读屏障( Load Barrier ) - 几乎整个 GC 过程都是并发的

5. 类加载机制是什么?双亲委派模型如何工作

类的生命周期:

Text Only
加载 → 验证 → 准备 → 解析 → 初始化 → 使用 → 卸载
      |←—— 连接(Linking) ——→|

各阶段详解:

  1. 加载( Loading ):读取.class 文件,生成 Class 对象
  2. 验证( Verification ):验证字节码格式、语义正确性
  3. 准备( Preparation ):为 static 变量分配内存并设置零值
Java
static int value = 123;  // 准备阶段value=0, 初始化阶段value=123
static final int CONST = 456;  // 编译期常量, 准备阶段就是456
  1. 解析( Resolution ):符号引用 → 直接引用
  2. 初始化( Initialization ):执行<clinit>方法(静态变量赋值+静态代码块)

双亲委派模型( Parent Delegation Model ):

Text Only
       Bootstrap ClassLoader (启动类加载器)
       加载: rt.jar (java.lang.*, java.util.* 等核心类)
              ↑  委托
       Extension ClassLoader (扩展类加载器)
       加载: jre/lib/ext 目录
              ↑  委托
       Application ClassLoader (应用类加载器)
       加载: classpath下的类
              ↑  委托
       Custom ClassLoader (自定义类加载器)
       加载: 自定义路径

双亲委派工作流程: 1. 收到类加载请求 2. 先委托给父类加载器 3. 父类加载器继续向上委托 4. 到顶层 Bootstrap ClassLoader ,如果能加载就加载 5. 父类无法加载,子类加载器才尝试加载

为什么需要双亲委派? - 安全性:防止用户自定义的类替换核心类(如自定义 java.lang.String ) - 避免重复加载:同一个类只加载一次

打破双亲委派的场景: 1. SPI 机制( Service Provider Interface ):如 JDBC , Bootstrap 加载的接口需要加载 classpath 下的实现类,使用线程上下文类加载器 2. OSGi:热部署/模块化 3. Tomcat:每个 Web 应用使用独立的类加载器,实现应用隔离 4. 自定义 ClassLoader:重写loadClass()findClass()方法

6. JVM 调优有哪些常用参数和工具

常用 JVM 参数:

Bash
# 堆内存
-Xms512m          # 初始堆大小
-Xmx2g            # 最大堆大小
-Xmn512m          # 年轻代大小
-XX:SurvivorRatio=8  # Eden:Survivor = 8:1

# 方法区/元空间
-XX:MetaspaceSize=256m
-XX:MaxMetaspaceSize=512m

# 栈
-Xss256k          # 线程栈大小

# GC收集器
-XX:+UseG1GC              # 使用G1
-XX:MaxGCPauseMillis=200   # G1目标停顿时间
-XX:+UseConcMarkSweepGC   # 使用CMS

# GC日志
-Xlog:gc*:gc.log:time     # JDK 9+
-XX:+PrintGCDetails        # JDK 8

# OOM时dump
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/tmp/heap.hprof

JVM 调优工具:

工具 用途
jps 列出 Java 进程
jstat GC 统计信息
jmap 堆内存快照(heap dump)
jstack 线程快照(thread dump)
jinfo JVM 参数查看/修改
jconsole 图形化监控
VisualVM 综合分析工具
Arthas 阿里开源诊断工具
MAT 堆快照分析(Eclipse Memory Analyzer)

常用命令:

Bash
jps -l                        # 列出Java进程
jstat -gc <pid> 1000          # 每秒输出GC统计
jmap -heap <pid>              # 堆概要信息
jmap -dump:format=b,file=heap.hprof <pid>  # dump堆
jstack <pid>                  # 线程快照

7. 如何排查内存泄漏

内存泄漏的常见原因: 1. 静态集合持有对象引用 2. 未关闭的资源( Connection 、 Stream ) 3. 监听器/回调未注销 4. ThreadLocal 未清理 5. 内部类持有外部类引用

排查步骤:

Text Only
1. 发现问题:
   - 应用频繁Full GC
   - OOM异常
   - jstat -gc 观察老年代持续增长

2. 获取堆快照:
   jmap -dump:format=b,file=heap.hprof <pid>
   或: -XX:+HeapDumpOnOutOfMemoryError (自动dump)

3. 分析堆快照 (MAT工具):
   - Leak Suspects Report: 自动分析可能的泄漏点
   - Dominator Tree: 找出占内存最多的对象
   - Histogram: 对象数量和大小统计
   - GC Roots: 查看对象的引用链

4. 定位代码:
   - 找到大量未释放的对象
   - 追踪其GC Root引用链
   - 定位到具体的代码位置

5. 修复:
   - 移除不必要的引用
   - 使用WeakReference/SoftReference
   - 正确关闭资源(try-with-resources)
   - 清理ThreadLocal

8. 什么是内存溢出( OOM )?有哪些类型

Text Only
java.lang.OutOfMemoryError: Java heap space
  → 堆内存不足
  → 解决: 增大-Xmx, 检查是否有内存泄漏

java.lang.OutOfMemoryError: Metaspace
  → 元空间不足(加载的类太多)
  → 解决: 增大-XX:MaxMetaspaceSize, 检查是否动态生成了过多类

java.lang.OutOfMemoryError: GC overhead limit exceeded
  → GC花费超过98%的时间但回收不到2%的内存
  → 解决: 检查内存泄漏

java.lang.StackOverflowError
  → 栈溢出(递归太深)
  → 解决: 减少递归深度或增大-Xss

java.lang.OutOfMemoryError: unable to create new native thread
  → 线程太多
  → 解决: 减少线程数, 增加操作系统线程限制

9. 什么是 JIT 编译器?它如何优化代码

JIT ( Just-In-Time )编译器: 将热点代码(频繁执行的字节码)编译为本地机器码,提高执行速度。

热点探测: - 方法调用计数器:方法被调用的次数 - 回边计数器:循环体执行的次数 - 超过阈值(默认 10000 次)触发 JIT 编译

JIT 优化技术: 1. 方法内联( Inlining ):将小方法的代码直接嵌入调用处 2. 逃逸分析( Escape Analysis ): - 对象没有逃逸出方法 → 栈上分配(避免 GC ) - 对象没有逃逸 → 标量替换(拆解为基本类型) - 对象没有逃逸 → 锁消除 3. 循环展开:减少循环判断次数 4. 公共子表达式消除

10. 强引用、软引用、弱引用、虚引用的区别

引用类型 回收时机 用途
强引用 永不回收(只要可达) 普通对象引用 直接引用
软引用 内存不足时回收 缓存 SoftReference
弱引用 下次 GC 时回收 缓存、 WeakHashMap WeakReference
虚引用 随时可回收 跟踪对象被回收的时机 PhantomReference
Java
// 强引用
Object obj = new Object();  // 只要obj存在, 对象不会被回收

// 软引用
SoftReference<Object> soft = new SoftReference<>(new Object());
soft.get();  // 内存不足前可获取, 内存不足时返回null

// 弱引用
WeakReference<Object> weak = new WeakReference<>(new Object());
weak.get();  // 下次GC前可获取

// ThreadLocal中Entry继承了WeakReference<ThreadLocal<?>>

11. 什么是字符串常量池

Java
// 字符串常量池在堆内存中(JDK 7+)

String s1 = "hello";     // 常量池中创建"hello"
String s2 = "hello";     // 直接引用常量池中的"hello"
System.out.println(s1 == s2);  // true (同一对象)

String s3 = new String("hello");  // 堆中新建对象
System.out.println(s1 == s3);     // false (不同对象)
System.out.println(s1.equals(s3)); // true (值相等)

String s4 = s3.intern();  // 返回常量池中的引用
System.out.println(s1 == s4);  // true

String 、 StringBuilder 、 StringBuffer 的区别:

维度 String StringBuilder StringBuffer
可变性 不可变 可变 可变
线程安全 安全(不可变) 不安全 安全(synchronized)
性能 拼接慢(每次创建新对象) 较快(有锁开销)
场景 少量操作 单线程拼接 多线程拼接(少用)

12. 直接内存( Direct Memory )是什么

Java
// 直接内存不在JVM堆中, 由操作系统管理
ByteBuffer buffer = ByteBuffer.allocateDirect(1024 * 1024);

// 优点: NIO时避免数据在堆和操作系统间拷贝(零拷贝)
// 缺点: 分配/释放比堆内存慢
// 大小受 -XX:MaxDirectMemorySize 限制
// 使用不当可能导致OOM

二、集合框架( 8 题)

13. HashMap 的底层原理是什么? put 过程是怎样的

HashMap 的数据结构( JDK 8 ):

Text Only
数组(Node[]) + 链表 + 红黑树

table: [null, null, Node, null, Node, ...]
                      |              |
                   Node→Node      Node
                      |
                   Node→...  当链表长度>8 → 转为红黑树

put 过程详解:

Java
// 简化的put流程:
public V put(K key, V value) {
    // 1. 计算hash: key的hashCode的高16位异或低16位
    int hash = hash(key);

    // 2. 计算数组下标: (n-1) & hash
    int index = (table.length - 1) & hash;

    // 3. 该位置为空 → 直接放入新Node
    if (table[index] == null) {
        table[index] = new Node(hash, key, value, null);
    }
    // 4. 该位置不为空
    else {
        // 4a. key已存在 → 覆盖value
        // 4b. 链表 → 尾插法加入链表末尾
        //     如果链表长度 >= 8 且数组长度 >= 64 → 链表转红黑树
        // 4c. 红黑树 → 按红黑树方式插入
    }

    // 5. 如果size > threshold(容量*负载因子) → 扩容
    if (++size > threshold) {
        resize();  // 扩容为原来的2倍
    }
}

扩容机制( resize ): - 默认初始容量: 16 - 默认负载因子: 0.75 - 扩容为原来的 2 倍 - 扩容时,元素的新下标要么不变,要么是原下标+旧容量

Text Only
原数组长度16: hash & 15 = hash & 0000 1111
新数组长度32: hash & 31 = hash & 0001 1111
多出来的那一位如果是0→位置不变, 是1→位置+16

为什么容量必须是 2 的幂? - (n-1) & hash 等价于 hash % n(位运算更快) - n 是 2 的幂时,(n-1)的低位全是 1 ,保证散列均匀

为什么链表长度 8 转红黑树? - 链表查找 O(n),红黑树 O(log n) - 选择 8 是因为:泊松分布下链表长度达到 8 的概率极低(约亿分之六) - 红黑树→链表的阈值是 6 (有个缓冲避免频繁转换)

14. HashMap 的哈希冲突如何解决

HashMap 使用链地址法(拉链法): - 哈希冲突的元素以链表形式存储在同一个桶中 - JDK 8 之后,链表过长(>8 )会转为红黑树

hash()函数的设计 — 扰动函数:

Java
static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    //                         高16位异或低16位
}
  • 让高位也参与计算,减少冲突
  • 因为数组长度通常不大( 16,32...),只用低位参与(n-1) & hash不够分散

HashMap 是否线程安全? - 不是线程安全的 - JDK 7 中多线程 resize 可能导致链表成环(死循环) - JDK 8 中虽然修复了环形链表,但仍可能数据覆盖

15. ConcurrentHashMap 的实现原理是什么? JDK 7 和 JDK 8 有什么区别

JDK 7 的实现 — 分段锁( Segment ):

Text Only
ConcurrentHashMap
├── Segment[0] (继承ReentrantLock)
│   └── HashEntry[] → 链表
├── Segment[1]
│   └── HashEntry[] → 链表
├── ...
└── Segment[15]  (默认16个Segment)
    └── HashEntry[] → 链表

每个Segment独立加锁, 最大并发数 = Segment数量

JDK 8 的实现 — CAS + synchronized :

Text Only
Node[] table (和HashMap类似: 数组+链表+红黑树)

put过程:
1. 计算hash, 找到桶位置
2. 桶为空 → CAS操作放入新Node (无锁)
3. 桶不为空 → synchronized锁住该桶的头节点
   - 链表: 遍历、插入/更新
   - 红黑树: 红黑树操作
4. 链表长度>8 → 转红黑树

锁粒度: 单个桶(Node), 比Segment更细, 并发更高

JDK 7 vs JDK 8 对比:

维度 JDK 7 JDK 8
数据结构 Segment + HashEntry 数组 + 链表 Node 数组 + 链表 + 红黑树
Segment(ReentrantLock) CAS + synchronized(Node)
锁粒度 段(Segment) 桶(Node)
并发度 Segment 数量(默认 16) 桶数量
查询复杂度 O(n) O(1)~O(log n)

16. ArrayList 和 LinkedList 的区别

维度 ArrayList LinkedList
底层结构 动态数组 双向链表
随机访问 O(1) ✅ O(n)
头部插入/删除 O(n)(需要移动元素) O(1) ✅
尾部插入 均摊 O(1) O(1)
中间插入/删除 O(n) O(1)(找到位置后)但找到位置 O(n)
内存占用 连续内存,较小 每个节点额外存储前后指针
缓存友好 ✅ 连续内存 ❌ 分散内存
实现的接口 List, RandomAccess List, Deque

ArrayList 扩容:

Java
// 初始容量10, 扩容为1.5倍
int newCapacity = oldCapacity + (oldCapacity >> 1);
// 扩容需要Arrays.copyOf, 拷贝整个数组

// 建议: 如果知道大致大小, 指定初始容量
List<String> list = new ArrayList<>(1000);

实际开发建议: - 绝大多数场景用 ArrayList (随机访问多,缓存友好) - 仅在频繁头部插入/删除时考虑 LinkedList - 作为队列/双端队列时可用 LinkedList (实现了 Deque 接口)

17. HashSet 的原理是什么

Java
// HashSet底层就是HashMap!
public class HashSet<E> {
    private transient HashMap<E, Object> map;
    private static final Object PRESENT = new Object();  // 占位value

    public boolean add(E e) {
        return map.put(e, PRESENT) == null;
    }

    public boolean contains(Object o) {
        return map.containsKey(o);
    }

    public boolean remove(Object o) {
        return map.remove(o) == PRESENT;
    }
}

// HashSet的元素就是HashMap的key
// 所有value都是同一个PRESENT对象

元素去重的原理: 1. 先计算 hashCode() 2. hashCode 相同 → 再调用 equals() 比较 3. 都相同 → 元素重复,不插入

自定义对象作为 HashSet 元素:

Java
class User {
    String name;
    int age;

    @Override
    public int hashCode() {
        return Objects.hash(name, age);  // 必须重写
    }

    @Override
    public boolean equals(Object o) {    // 必须重写
        if (this == o) return true;
        if (!(o instanceof User)) return false;
        User user = (User) o;
        return age == user.age && Objects.equals(name, user.name);
    }
}

18. Iterator 和 Iterable 的区别? fail-fast 机制是什么

Java
// Iterable: 表示可迭代, 提供iterator()方法
public interface Iterable<T> {  // interface定义类型契约
    Iterator<T> iterator();  // 泛型<T>:类型参数化
}

// Iterator: 迭代器, 负责遍历
public interface Iterator<E> {
    boolean hasNext();
    E next();
    default void remove() { ... }
}

// 实现Iterable的类可以用for-each
for (String s : list) { ... }
// 编译后等价于:
Iterator<String> it = list.iterator();
while (it.hasNext()) {
    String s = it.next();
}

fail-fast 机制:

Java
List<String> list = new ArrayList<>(List.of("a", "b", "c"));

// ❌ 遍历时修改集合会抛出ConcurrentModificationException
for (String s : list) {
    if ("b".equals(s)) {
        list.remove(s);  // ConcurrentModificationException!
    }
}

// ✅ 使用Iterator的remove
Iterator<String> it = list.iterator();
while (it.hasNext()) {
    if ("b".equals(it.next())) {
        it.remove();  // 安全删除
    }
}

// ✅ 使用removeIf
list.removeIf(s -> "b".equals(s));

原理: 集合内部维护一个modCount(修改次数),迭代器创建时记录expectedModCount。每次next()检查modCount == expectedModCount,不一致则抛出异常。

19. TreeMap 和 LinkedHashMap 的特点

TreeMap : - 基于红黑树实现 - 按 key 自然排序或指定 Comparator 排序 - put/get: O(log n) - 适用于需要排序的场景

LinkedHashMap : - 在 HashMap 基础上维护了双向链表 - 两种排序模式: - 插入顺序(默认) - 访问顺序( accessOrder=true, 可用于实现 LRU 缓存)

Java
// LRU缓存实现
class LRUCache<K, V> extends LinkedHashMap<K, V> {
    private final int capacity;

    public LRUCache(int capacity) {
        super(capacity, 0.75f, true);  // accessOrder=true
        this.capacity = capacity;
    }

    @Override
    protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
        return size() > capacity;  // 超过容量时移除最久未访问的
    }
}

20. Java 8 中集合的新特性

Java
// Stream API
List<String> names = users.stream()
    .filter(u -> u.getAge() > 18)
    .sorted(Comparator.comparing(User::getAge))
    .map(User::getName)
    .distinct()
    .limit(10)
    .collect(Collectors.toList());

// Map新方法
map.getOrDefault(key, defaultValue);
map.putIfAbsent(key, value);
map.computeIfAbsent(key, k -> new ArrayList<>());
map.merge(key, value, (v1, v2) -> v1 + v2);
map.forEach((k, v) -> System.out.println(k + "=" + v));

// Collection新方法
list.removeIf(s -> s.isEmpty());
list.replaceAll(String::toUpperCase);
list.sort(Comparator.naturalOrder());

// 不可变集合(JDK 9+)
List<String> list = List.of("a", "b", "c");
Map<String, Integer> map = Map.of("a", 1, "b", 2);
Set<String> set = Set.of("a", "b", "c");

三、并发编程( 12 题)

21. synchronized 的原理是什么?锁升级过程是怎样的

synchronized 的使用方式:

Java
// 1. 同步实例方法 → 锁住this对象
public synchronized void method() { ... }

// 2. 同步静态方法 → 锁住Class对象
public static synchronized void method() { ... }

// 3. 同步代码块 → 锁住指定对象
synchronized (lockObj) { ... }

底层原理 — Monitor :

Text Only
每个Java对象都关联一个Monitor(管程/监视器)

synchronized代码块 → monitorenter / monitorexit 指令
synchronized方法  → ACC_SYNCHRONIZED 标志

Monitor结构:
├── _owner:    持有锁的线程
├── _count:    重入次数
├── _EntryList: 等待获取锁的线程队列(阻塞)
└── _WaitSet:   调用wait()后等待的线程集合

锁升级过程( JDK 6 优化):

Text Only
无锁 → 偏向锁 → 轻量级锁 → 重量级锁
              ↑          ↑
         第一次获取    竞争激烈
         偏向当前线程  CAS自旋

1. 偏向锁( Biased Locking ): - 适用:只有一个线程访问同步块 - 实现:在 Mark Word 中记录偏向的线程 ID - 下次同线程进入:无需任何同步操作(零开销) - 其他线程尝试获取 → 撤销偏向锁 → 升级为轻量级锁 - JDK 15 默认禁用偏向锁

2. 轻量级锁: - 适用:多线程交替执行,无实际竞争 - 实现:在线程栈帧中创建 Lock Record , CAS 尝试把对象的 Mark Word 指向 Lock Record - CAS 成功 → 获取轻量级锁 - CAS 失败 → 说明有竞争,自旋等待 - 自旋超过阈值 → 升级为重量级锁

3. 重量级锁: - 适用:高并发竞争 - 实现:基于操作系统的 Mutex Lock - 没获取到锁的线程被阻塞(内核态/用户态切换,开销大)

22. ReentrantLock 和 synchronized 有什么区别

维度 synchronized ReentrantLock
实现 JVM 内置(字节码指令) JDK 实现( AQS )
释放方式 自动释放(退出同步块) 手动 release()
中断 不可中断 lockInterruptibly()可中断
超时 不支持 tryLock(timeout)支持
公平锁 不支持 支持(new ReentrantLock(true))
条件变量 只有一个 wait/notify 多个 Condition
可重入
性能 JDK 6 优化后差不多 差不多
Java
// ReentrantLock使用
ReentrantLock lock = new ReentrantLock();

lock.lock();
try {
    // 临界区
} finally {
    lock.unlock();  // 必须在finally中释放
}

// 尝试获取锁,超时返回false
if (lock.tryLock(5, TimeUnit.SECONDS)) {
    try {
        // 获取成功
    } finally {
        lock.unlock();
    }
} else {
    // 获取超时
}

// Condition
Condition notFull = lock.newCondition();
Condition notEmpty = lock.newCondition();

lock.lock();
try {
    while (queue.isFull()) {
        notFull.await();    // 等待"不满"条件
    }
    queue.add(item);
    notEmpty.signal();       // 通知"不空"
} finally {
    lock.unlock();
}

23. volatile 关键字的作用和原理是什么

volatile 的三个特性:

1. 可见性: 一个线程对 volatile 变量的修改对其他线程立即可见

Java
// 不用volatile → 线程B可能永远看不到flag的变化
// 用volatile → 线程B能立即看到flag的变化
volatile boolean flag = false;

// 线程A
flag = true;

// 线程B
while (!flag) {  // 能看到flag变为true
    // ...
}

2. 有序性: 禁止指令重排序

Java
// 典型问题: 双检锁单例
class Singleton {
    // 必须加volatile! 防止指令重排
    private static volatile Singleton instance;

    public static Singleton getInstance() {
        if (instance == null) {                 // 第一次检查
            synchronized (Singleton.class) {  // synchronized同步锁,保证线程安全
                if (instance == null) {           // 第二次检查
                    instance = new Singleton();   // ①分配内存 ②初始化 ③赋值
                    // 不加volatile: ②③可能重排为③②
                    // 导致其他线程拿到未初始化的对象
                }
            }
        }
        return instance;
    }
}

3. volatile 不保证原子性

Java
volatile int count = 0;

// ❌ count++不是原子操作(读→改→写)
// 多线程下count++仍会出问题
count++;  // 实际是: temp = count; temp = temp + 1; count = temp;

// ✅ 使用AtomicInteger
AtomicInteger count = new AtomicInteger(0);
count.incrementAndGet();

volatile 的内存屏障实现:

Text Only
写volatile变量:
  StoreStore屏障
  volatile写
  StoreLoad屏障    ← 保证volatile写对后续读可见

读volatile变量:
  LoadLoad屏障
  volatile读
  LoadStore屏障    ← 保证volatile读之后的操作不会重排到读之前

24. 线程池的原理是什么? 7 个核心参数分别是什么

Java
public ThreadPoolExecutor(
    int corePoolSize,        // 核心线程数 (即使空闲也不回收)
    int maximumPoolSize,     // 最大线程数
    long keepAliveTime,      // 非核心线程的空闲存活时间
    TimeUnit unit,           // 存活时间单位
    BlockingQueue<Runnable> workQueue,  // 任务队列
    ThreadFactory threadFactory,        // 线程工厂(命名等)
    RejectedExecutionHandler handler    // 拒绝策略
)

线程池工作流程:

Text Only
提交任务
当前线程数 < corePoolSize?
   ├── 是 → 创建核心线程执行任务
   └── 否 → 任务队列满了?
              ├── 否 → 加入任务队列等待
              └── 是 → 当前线程数 < maximumPoolSize?
                        ├── 是 → 创建非核心线程执行任务
                        └── 否 → 执行拒绝策略

4 种拒绝策略:

策略 行为
AbortPolicy 抛出 RejectedExecutionException (默认)
CallerRunsPolicy 由提交任务的线程执行(降级)
DiscardPolicy 静默丢弃任务
DiscardOldestPolicy 丢弃队列中最老的任务,重新提交

线程数设置建议:

Text Only
CPU密集型: corePoolSize = CPU核心数 + 1
IO密集型:  corePoolSize = CPU核心数 * 2 (或 CPU核心数 / (1 - IO时间占比))

常用线程池(不推荐直接使用 Executors 创建):

Java
// ❌ 不推荐(队列无界, 可能OOM)
Executors.newFixedThreadPool(10);      // LinkedBlockingQueue(无界)
Executors.newSingleThreadExecutor();   // LinkedBlockingQueue(无界)
Executors.newCachedThreadPool();       // SynchronousQueue, 线程数无上限

// ✅ 推荐手动创建
new ThreadPoolExecutor(
    10, 20, 60L, TimeUnit.SECONDS,
    new ArrayBlockingQueue<>(1000),
    new ThreadPoolExecutor.CallerRunsPolicy()
);

execute vs submit :

维度 execute submit
返回值 void Future<?>
异常处理 直接抛出 封装在 Future 中
参数 Runnable Runnable / Callable

25. CompletableFuture 的使用

Java
// 异步执行
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
    return fetchData();  // 异步获取数据
});

// 链式操作
CompletableFuture<String> result = CompletableFuture
    .supplyAsync(() -> getUserId())          // 异步执行
    .thenApply(id -> getUser(id))            // 同步转换
    .thenApplyAsync(user -> format(user))    // 异步转换
    .exceptionally(e -> "默认值");           // 异常处理

// 多个异步任务组合
CompletableFuture<String> future1 = CompletableFuture.supplyAsync(() -> fetchFromDB());
CompletableFuture<String> future2 = CompletableFuture.supplyAsync(() -> fetchFromAPI());

// 等待所有完成
CompletableFuture.allOf(future1, future2).join();

// 等待任一完成
CompletableFuture.anyOf(future1, future2).thenAccept(System.out::println);

// 两个结果合并
future1.thenCombine(future2, (r1, r2) -> r1 + r2);

26. AQS ( AbstractQueuedSynchronizer )原理是什么

AQS 是 Java 并发包的基础框架, ReentrantLock 、 Semaphore 、 CountDownLatch 等都基于 AQS 实现。

核心结构:

Text Only
AQS:
├── state (int)     : 同步状态
│   - 0: 锁未被持有
│   - >0: 锁被持有(可重入, state=重入次数)
├── exclusiveOwnerThread: 持有锁的线程
└── CLH队列(双向链表): 排队等待获取锁的线程
    head ↔ Node(t1) ↔ Node(t2) ↔ Node(t3) ↔ tail

获取锁的过程(以 ReentrantLock 为例):

Text Only
1. 尝试CAS修改state: 0 → 1
   ├── 成功 → 获取锁, 设置exclusiveOwnerThread
   └── 失败 →
       2. 检查是否可重入(当前线程==owner) → state+1
       3. 不可重入 → 创建Node加入CLH队列尾部(CAS)
       4. 在队列中自旋/阻塞(park)等待
       5. 前驱节点释放锁时唤醒(unpark)

公平锁 vs 非公平锁: - 非公平锁:新线程先尝试 CAS 获取,失败才排队(可能插队,吞吐量高) - 公平锁:直接排队,按 FIFO 顺序获取锁(不会饿死,但吞吐量低)

27. CAS 的原理是什么? ABA 问题如何解决

CAS ( Compare And Swap ):

Java
// 伪代码
boolean compareAndSwap(V expected, V newValue) {
    if (当前值 == expected) {
        当前值 = newValue;
        return true;   // 成功
    }
    return false;        // 失败, 需要重试
}

// CAS是CPU原子指令(cmpxchg), 不需要加锁

Java 中的 CAS — Unsafe 类:

Java
// AtomicInteger的incrementAndGet
public final int incrementAndGet() {
    return U.getAndAddInt(this, VALUE, 1) + 1;
}

// 自旋重试
public final int getAndAddInt(Object o, long offset, int delta) {
    int v;
    do {
        v = getIntVolatile(o, offset);        // 读取当前值
    } while (!compareAndSwapInt(o, offset, v, v + delta));  // CAS更新
    return v;
}

ABA 问题:

Text Only
线程1: 读取值A
线程2: A → B → A (改了两次又改回来)
线程1: CAS发现仍然是A → 成功 (但实际已经被修改过了)

解决方案 — AtomicStampedReference :

Java
// 加版本号
AtomicStampedReference<Integer> ref = new AtomicStampedReference<>(1, 0);

int stamp = ref.getStamp();  // 获取版本号
int value = ref.getReference();

// CAS时同时检查值和版本号
ref.compareAndSet(value, newValue, stamp, stamp + 1);

28. ThreadLocal 的原理是什么?如何避免内存泄漏

ThreadLocal 原理:

Text Only
每个Thread对象中有一个ThreadLocalMap:

Thread:
└── ThreadLocalMap:
    ├── Entry(ThreadLocal_A → value_A)
    ├── Entry(ThreadLocal_B → value_B)
    └── ...

Entry继承WeakReference<ThreadLocal>:
  key是ThreadLocal的弱引用
  value是强引用
Java
ThreadLocal<User> userContext = new ThreadLocal<>();

// 设置当前线程的值
userContext.set(currentUser);

// 获取当前线程的值
User user = userContext.get();

// 使用完必须remove!
userContext.remove();

内存泄漏问题:

Text Only
ThreadLocal对象被GC回收后:
Entry的key(WeakReference)变为null
但Entry的value仍然被强引用

Thread → ThreadLocalMap → Entry → value (无法GC!)

如果线程是线程池中的长期线程,ThreadLocalMap不会被清理
→ value永远不会被回收 → 内存泄漏

解决方案:

Java
// 使用完后一定要remove!
try {
    threadLocal.set(value);
    // 业务逻辑
} finally {
    threadLocal.remove();  // 必须清理
}

// 或使用InheritableThreadLocal (父线程传递给子线程)
// 或使用TransmittableThreadLocal (线程池场景)

29. CountDownLatch 、 CyclicBarrier 和 Semaphore 的区别

CountDownLatch (计数器,一次性):

Java
// 等待N个任务完成
CountDownLatch latch = new CountDownLatch(3);

// 工作线程
for (int i = 0; i < 3; i++) {
    executor.execute(() -> {
        doWork();
        latch.countDown();  // 计数减1
    });
}

latch.await();  // 主线程等待, 直到计数为0
System.out.println("所有任务完成");

CyclicBarrier (屏障,可重用):

Java
// N个线程互相等待, 到齐后一起继续
CyclicBarrier barrier = new CyclicBarrier(3, () -> {
    System.out.println("所有线程到达栅栏, 一起出发!");  // 到齐后执行
});

for (int i = 0; i < 3; i++) {
    executor.execute(() -> {
        prepare();
        barrier.await();  // 等待其他线程到达
        execute();         // 所有线程到齐后一起执行
    });
}

Semaphore (信号量,限流):

Java
// 限制并发数量
Semaphore semaphore = new Semaphore(3);  // 最多3个并发

for (int i = 0; i < 10; i++) {
    executor.execute(() -> {
        semaphore.acquire();  // 获取许可(可用许可-1)
        try {  // try/catch捕获异常
            accessResource();  // 最多3个线程同时执行
        } finally {
            semaphore.release();  // 释放许可
        }
    });
}
维度 CountDownLatch CyclicBarrier Semaphore
作用 等待 N 个事件完成 N 个线程互相等待 限制并发数
可重用 不可重用 可重用(reset) 可重用
线程角色 等待者 vs 计数者 所有线程平等 竞争许可
计数方向 递减到 0 递增到 N 增减

30. Java 中的阻塞队列有哪些

队列 底层 有界 特点
ArrayBlockingQueue 数组 有界 公平/非公平锁
LinkedBlockingQueue 链表 可选(默认 MAX) 两把锁(头尾分离)
PriorityBlockingQueue 无界 优先级排序
SynchronousQueue 无存储 0 容量 直接交换(生产者阻塞直到消费者获取)
DelayQueue PriorityQueue 无界 延迟获取
Java
BlockingQueue<String> queue = new ArrayBlockingQueue<>(100);

// 阻塞方法
queue.put("item");     // 队列满时阻塞
queue.take();          // 队列空时阻塞

// 非阻塞方法
queue.offer("item");   // 满时返回false
queue.poll();          // 空时返回null

// 超时方法
queue.offer("item", 1, TimeUnit.SECONDS);  // 满时等1秒
queue.poll(1, TimeUnit.SECONDS);            // 空时等1秒

31. 什么是线程安全?如何保证线程安全

线程安全: 多线程环境下代码的执行结果与预期一致。

保证线程安全的方式:

  1. 互斥同步: synchronized 、 ReentrantLock
  2. 非阻塞同步: CAS ( Atomic 类)
  3. 无同步方案
  4. ThreadLocal (线程隔离)
  5. 不可变对象( final + String + 不可变集合)
  6. 栈封闭(局部变量,天然线程安全)

32. 线程的生命周期和状态变化

Text Only
        ┌────────────────────────────────────────────┐
        │                                            │
        ↓                                            │
  NEW ─→ RUNNABLE ←→ BLOCKED                        │
   |        ↕          ↕                             │
   |      WAITING    TIMED_WAITING                   │
   |        ↕          ↕                             │
   |      RUNNABLE *→ TERMINATED ←───────────────────┘
状态 说明 进入方式
NEW 新建,未启动 new Thread()
RUNNABLE 可运行(包含就绪和运行) start()
BLOCKED 等待获取 monitor 锁 等待 synchronized
WAITING 无限期等待 wait()、 join()、 LockSupport.park()
TIMED_WAITING 有超时的等待 sleep(ms)、 wait(ms)、 join(ms)
TERMINATED 终止 run()执行完毕

四、 Spring ( 8 题)

33. Spring IoC 的原理是什么? Bean 的生命周期是怎样的

IoC ( Inversion of Control )控制反转: - 对象的创建和依赖关系的管理交给 Spring 容器 - 开发者不再手动 new 对象,而是通过容器注入

DI ( Dependency Injection )依赖注入方式:

Java
// 1. 构造器注入(推荐)
@Component
public class UserService {
    private final UserRepository userRepo;

    @Autowired  // Spring 4.3+单构造器可省略
    public UserService(UserRepository userRepo) {
        this.userRepo = userRepo;
    }
}

// 2. Setter注入
@Autowired
public void setUserRepo(UserRepository userRepo) {
    this.userRepo = userRepo;
}

// 3. 字段注入(不推荐,无法final,测试不便)
@Autowired
private UserRepository userRepo;

BeanFactory vs ApplicationContext : - BeanFactory :最基础的 IoC 容器,懒加载 - ApplicationContext :扩展了 BeanFactory ,立即加载,提供 AOP 、事件、国际化等

Bean 的完整生命周期:

Text Only
1. 实例化(Instantiation)
   ↓ 通过构造器创建Bean实例
2. 属性填充(Populate Properties)
   ↓ 注入依赖(@Autowired)
3. Aware接口回调
   ↓ BeanNameAware, BeanFactoryAware, ApplicationContextAware
4. BeanPostProcessor.postProcessBeforeInitialization()
   ↓ (如@PostConstruct在这里执行)
5. InitializingBean.afterPropertiesSet()
6. 自定义init-method
7. BeanPostProcessor.postProcessAfterInitialization()
   ↓ (AOP代理在这里创建)
8. Bean就绪,可以使用
   ↓ ... 使用中 ...
9. DisposableBean.destroy()
10. 自定义destroy-method / @PreDestroy

Bean 作用域: | 作用域 | 说明 | |--------|------| | singleton | 单例(默认),整个容器一个实例 | | prototype | 原型,每次获取创建新实例 | | request | 每个 HTTP 请求一个实例 | | session | 每个 HTTP Session 一个实例 |

34. Spring AOP 的原理是什么? JDK 动态代理和 CGLIB 的区别

AOP ( Aspect-Oriented Programming )面向切面编程: 在不修改源代码的情况下,给方法添加额外的功能(如日志、事务、权限检查)。

AOP 核心概念: - 切面( Aspect ):横切关注点的模块化(如日志切面) - 切入点( Pointcut ):定义哪些方法需要增强 - 通知( Advice ):增强的具体逻辑 - 连接点( JoinPoint ):可以被增强的位置(方法执行时)

5 种通知类型:

Java
@Aspect
@Component
public class LogAspect {

    @Before("execution(* com.example.service.*.*(..))")
    public void before(JoinPoint jp) {
        // 方法执行前
    }

    @After("execution(* com.example.service.*.*(..))")
    public void after(JoinPoint jp) {
        // 方法执行后(无论是否异常)
    }

    @AfterReturning(pointcut = "execution(...)", returning = "result")
    public void afterReturning(Object result) {
        // 方法正常返回后
    }

    @AfterThrowing(pointcut = "execution(...)", throwing = "ex")
    public void afterThrowing(Exception ex) {
        // 方法抛出异常后
    }

    @Around("execution(* com.example.service.*.*(..))")
    public Object around(ProceedingJoinPoint pjp) throws Throwable {
        // 方法执行前
        long start = System.currentTimeMillis();
        Object result = pjp.proceed();  // 执行目标方法
        // 方法执行后
        long cost = System.currentTimeMillis() - start;
        return result;
    }
}

JDK 动态代理 vs CGLIB :

维度 JDK 动态代理 CGLIB
实现方式 基于接口( java.lang.reflect.Proxy ) 基于继承(生成子类)
要求 目标类必须实现接口 目标类不能是 final
性能 JDK 8+性能接近 CGLIB 较好
Spring 默认 有接口时默认 JDK 代理 无接口时使用 CGLIB

Spring Boot 2.x 默认使用 CGLIB 代理。

35. Spring Boot 自动配置的原理是什么

核心注解:@SpringBootApplication

Java
@SpringBootApplication
// 等价于:
@SpringBootConfiguration  // = @Configuration, 标识为配置类
@EnableAutoConfiguration  // ⭐ 开启自动配置
@ComponentScan            // 包扫描

自动配置流程:

Text Only
1. @EnableAutoConfiguration
2. @Import(AutoConfigurationImportSelector.class)
3. 读取 META-INF/spring.factories (Spring Boot 2.x)
   或 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports (3.x)
4. 获取所有AutoConfiguration类的全限定名
5. 条件注解过滤 (根据classpath中是否有相关类/配置决定是否生效)
   @ConditionalOnClass(DataSource.class)      // classpath有此类才生效
   @ConditionalOnMissingBean(DataSource.class) // 用户没自定义才生效
   @ConditionalOnProperty(name="...", havingValue="true")
6. 满足条件的AutoConfiguration类被加载, 自动配置Bean

示例: DataSourceAutoConfiguration

Java
@Configuration
@ConditionalOnClass(DataSource.class)  // classpath有DataSource
@ConditionalOnMissingBean(DataSource.class)  // 用户没自定义
@EnableConfigurationProperties(DataSourceProperties.class)  // 读取配置
public class DataSourceAutoConfiguration {

    @Bean
    public DataSource dataSource(DataSourceProperties properties) {
        // 根据配置创建数据源
    }
}

自定义启用/禁用:

YAML
# application.yml
spring:
  autoconfigure:
    exclude:
      - org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration

36. Spring 事务管理的原理是什么

声明式事务:

Java
@Transactional(
    propagation = Propagation.REQUIRED,     // 传播行为
    isolation = Isolation.DEFAULT,          // 隔离级别
    timeout = 30,                           // 超时(秒)
    readOnly = false,                       // 只读
    rollbackFor = Exception.class           // 回滚条件
)
public void transfer(Long from, Long to, BigDecimal amount) {
    accountDao.debit(from, amount);
    accountDao.credit(to, amount);
}

7 种传播行为:

传播行为 说明
REQUIRED 有事务就加入,没有就新建(默认)
REQUIRES_NEW 总是新建事务,挂起当前事务
NESTED 有事务就创建嵌套事务(保存点)
SUPPORTS 有事务就加入,没有就非事务执行
NOT_SUPPORTED 非事务执行,挂起当前事务
MANDATORY 必须在事务中调用,否则抛异常
NEVER 不能在事务中调用,否则抛异常

事务失效的常见场景:

  1. 方法不是 public: Spring 事务基于 AOP 代理,非 public 方法不会被代理
  2. 自调用:同一个类中方法 A 调方法 B ,不走代理
Java
public class UserService {
    public void methodA() {
        this.methodB();  // ❌ 自调用, 不走代理, 事务失效
    }
    @Transactional
    public void methodB() { ... }
}
  1. 异常被 catch 了:事务不知道发生了异常
  2. 抛出非 RuntimeException:默认只回滚 RuntimeException
  3. 数据库引擎不支持事务(如 MyISAM )
  4. Bean 未被 Spring 管理

37. Spring 循环依赖是如何解决的

什么是循环依赖:

Java
@Service
public class A {
    @Autowired
    private B b;  // A依赖B
}

@Service
public class B {
    @Autowired
    private A a;  // B依赖A
}

Spring 通过三级缓存解决单例 Bean 的循环依赖:

Java
// DefaultSingletonBeanRegistry
Map<String, Object> singletonObjects;           // 一级缓存: 成品Bean
Map<String, Object> earlySingletonObjects;      // 二级缓存: 早期Bean(可能是代理)
Map<String, ObjectFactory<?>> singletonFactories; // 三级缓存: Bean工厂

解决过程:

Text Only
1. 创建A → A实例化(new A()) → 放入三级缓存(ObjectFactory)
2. A填充属性 → 发现需要B
3. 创建B → B实例化(new B()) → 放入三级缓存
4. B填充属性 → 发现需要A
5. 从三级缓存获取A的ObjectFactory → 生成早期A → 放入二级缓存
6. B注入早期A → B初始化完成 → 放入一级缓存
7. 回到A → 注入B → A初始化完成 → 放入一级缓存

三级缓存的作用: 三级缓存(ObjectFactory)可以在需要时生成代理对象。如果 A 需要 AOP 代理, ObjectFactory 会返回代理对象而非原始对象。

无法解决循环依赖的情况: - 构造器注入的循环依赖(实例化时就需要依赖,无法创建早期对象) - prototype 作用域的循环依赖

38. Spring MVC 的请求处理流程是怎样的

JavaScript
1. 客户端发送HTTP请求
   
2. DispatcherServlet(前端控制器) 接收请求
   
3. HandlerMapping 根据URL找到对应的Controller和方法
    返回 HandlerExecutionChain(Handler + 拦截器)
4. HandlerAdapter 适配并执行Handler(Controller方法)
   
5. Controller 执行业务逻辑, 返回ModelAndView
   
6. ViewResolver 解析视图名称  找到对应的View
   
7. View 渲染页面(或直接返回JSON: @ResponseBody)
   
8. DispatcherServlet 返回响应给客户端

拦截器( Interceptor ):

Java
@Component
public class AuthInterceptor implements HandlerInterceptor {  // extends继承;implements实现接口

    @Override  // @Override重写父类方法
    public boolean preHandle(HttpServletRequest request,
                            HttpServletResponse response, Object handler) {
        // 请求处理前 (如权限检查)
        return true;  // true继续, false中断
    }

    @Override
    public void postHandle(HttpServletRequest request,
                          HttpServletResponse response, Object handler,
                          ModelAndView modelAndView) {
        // 请求处理后, 渲染前
    }

    @Override
    public void afterCompletion(HttpServletRequest request,
                               HttpServletResponse response, Object handler,
                               Exception ex) {
        // 请求完成后 (如资源清理)
    }
}

39. Spring Boot Starter 的原理是什么

Text Only
spring-boot-starter-web 包含:
├── spring-boot-starter (核心)
├── spring-web           (Web框架)
├── spring-webmvc        (MVC)
├── spring-boot-starter-tomcat  (内嵌Tomcat)
├── spring-boot-starter-json    (Jackson)
└── ...

引入starter后自动配置:
1. 依赖自动引入 (pom.xml中的依赖传递)
2. AutoConfiguration类根据条件注解自动生效
3. 开发者只需在application.yml中配置参数

自定义 Starter :

Text Only
my-spring-boot-starter/
├── pom.xml (依赖spring-boot-starter)
├── src/main/java/
│   └── com/example/
│       ├── MyAutoConfiguration.java
│       └── MyProperties.java
└── src/main/resources/
    └── META-INF/
        └── spring.factories (或 AutoConfiguration.imports)

40. Spring 中常用的设计模式有哪些

设计模式 Spring 中的体现
工厂模式 BeanFactory 、 ApplicationContext
单例模式 Bean 默认单例作用域
代理模式 AOP (JDK 动态代理/CGLIB)
模板方法 JdbcTemplate 、 RestTemplate
观察者模式 ApplicationEvent 、 ApplicationListener
适配器模式 HandlerAdapter
策略模式 Resource 接口的多种实现
责任链模式 拦截器链(Interceptor)
装饰器模式 BeanWrapper

面试答题技巧

  1. JVM 题:从内存结构→GC 算法→GC 收集器→调优,层层递进
  2. 集合题:讲清底层数据结构和关键方法实现(如 HashMap 的 put )
  3. 并发题:先讲原理( AQS/CAS ),再讲工具类的使用和区别
  4. Spring 题:从 IoC/AOP 原理出发,结合源码讲解
  5. 结合实际:讲解 OOM 排查经历、线程池参数调优等实际案例

⚠️ 核验说明(2026-04-03):本页已完成 2026-04-03 人工复核。术语里带“必须”的表述主要保留在 Java 语义、并发安全前提和框架约束本身成立的地方;工具示例继续按常见生态保留。


最后更新日期: 2026-04-03