跳转至

Java面试题

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
    
  4. 解析(Resolution):符号引用 → 直接引用
  5. 初始化(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() { ... }
    }
    
  3. 异常被catch了:事务不知道发生了异常
  4. 抛出非RuntimeException:默认只回滚RuntimeException
  5. 数据库引擎不支持事务(如MyISAM)
  6. 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排查经历、线程池参数调优等实际案例

最后更新:2025年