Java面试题¶
40道Java面试高频题 + 详细解答,覆盖JVM、集合框架、并发编程、Spring等核心知识点。
一、JVM(12题)¶
1. JVM内存结构是怎样的?各区域的作用分别是什么?¶
JVM运行时数据区:
┌─────────────────────────────────────────────┐
│ JVM内存 │
│ │
│ ┌─────────────────────────────────────┐ │
│ │ 线程共享区域 │ │
│ │ ┌────────────┐ ┌──────────────┐ │ │
│ │ │ 堆(Heap) │ │ 方法区/元空间 │ │ │
│ │ │ 对象实例 │ │ 类信息/常量池 │ │ │
│ │ │ 数组 │ │ 静态变量 │ │ │
│ │ └────────────┘ └──────────────┘ │ │
│ └─────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────┐ │
│ │ 线程私有区域 │ │
│ │ ┌──────┐ ┌────────┐ ┌──────────┐ │ │
│ │ │虚拟机栈│ │程序计数器 │ │本地方法栈 │ │ │
│ │ │局部变量│ │当前指令 │ │ Native │ │ │
│ │ │操作数栈│ │ 地址 │ │ 方法调用 │ │ │
│ │ └──────┘ └────────┘ └──────────┘ │ │
│ └─────────────────────────────────────┘ │
└─────────────────────────────────────────────┘
各区域详解:
1. 堆(Heap)— 线程共享 - 存储对象实例和数组 - JVM最大的一块内存区域 - GC的主要工作区域 - 分为年轻代(Young)和老年代(Old)
堆内存:
├── 年轻代(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. 对象创建的过程是什么?对象在内存中的布局?¶
对象创建过程:
new Object()
↓
1. 类加载检查: 检查类是否已加载
↓ (如未加载则先加载类)
2. 分配内存: 在堆中分配对象所需内存
- 指针碰撞(Bump the Pointer): 堆内存规整时
- 空闲列表(Free List): 堆内存不规整时
- TLAB(Thread Local Allocation Buffer): 线程本地缓冲区, 避免并发
↓
3. 内存初始化为零值: int→0, boolean→false, 引用→null
↓
4. 设置对象头: 类指针、哈希码、GC年龄、锁标志
↓
5. 执行<init>: 调用构造方法初始化
对象的内存布局:
┌──────────────────────────────┐
│ 对象头(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)
2. 复制算法(Copying)
将内存分为两块(From和To)
只使用From区分配对象
GC时: 将From中存活对象复制到To区,然后清空From
交换From和To的角色
优点: 无碎片,分配效率高
缺点: 内存利用率只有50%
适用: 年轻代(大部分对象朝生夕死, 存活率低, 复制开销小)
3. 标记-整理(Mark-Compact)
4. 分代收集(Generational Collection)
年轻代: 复制算法 (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收集器四个阶段:
1. 初始标记(STW) : 标记GC Roots直接关联的对象 → 很快
2. 并发标记(并发) : 从GC Roots遍历整个对象图 → 耗时但并发
3. 重新标记(STW) : 修正并发标记期间产生变动的对象 → 较快
4. 并发清除(并发) : 清除不可达对象 → 耗时但并发
CMS缺点: 产生碎片、浮动垃圾、CPU敏感
G1收集器:
- 将堆划分为多个等大的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. 类加载机制是什么?双亲委派模型如何工作?¶
类的生命周期:
各阶段详解:
- 加载(Loading):读取.class文件,生成Class对象
- 验证(Verification):验证字节码格式、语义正确性
- 准备(Preparation):为static变量分配内存并设置零值
- 解析(Resolution):符号引用 → 直接引用
- 初始化(Initialization):执行
<clinit>方法(静态变量赋值+静态代码块)
双亲委派模型(Parent Delegation Model):
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参数:
# 堆内存
-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) |
常用命令:
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. 内部类持有外部类引用
排查步骤:
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)?有哪些类型?¶
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 |
// 强引用
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. 什么是字符串常量池?¶
// 字符串常量池在堆内存中(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)是什么?¶
// 直接内存不在JVM堆中, 由操作系统管理
ByteBuffer buffer = ByteBuffer.allocateDirect(1024 * 1024);
// 优点: NIO时避免数据在堆和操作系统间拷贝(零拷贝)
// 缺点: 分配/释放比堆内存慢
// 大小受 -XX:MaxDirectMemorySize 限制
// 使用不当可能导致OOM
二、集合框架(8题)¶
13. HashMap的底层原理是什么?put过程是怎样的?¶
HashMap的数据结构(JDK 8):
数组(Node[]) + 链表 + 红黑树
table: [null, null, Node, null, Node, ...]
| |
Node→Node Node
|
Node→... 当链表长度>8 → 转为红黑树
put过程详解:
// 简化的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倍 - 扩容时,元素的新下标要么不变,要么是原下标+旧容量
原数组长度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()函数的设计 — 扰动函数:
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
// 高16位异或低16位
}
(n-1) & hash不够分散 HashMap是否线程安全? - 不是线程安全的 - JDK 7中多线程resize可能导致链表成环(死循环) - JDK 8中虽然修复了环形链表,但仍可能数据覆盖
15. ConcurrentHashMap的实现原理是什么?JDK 7和JDK 8有什么区别?¶
JDK 7的实现 — 分段锁(Segment):
ConcurrentHashMap
├── Segment[0] (继承ReentrantLock)
│ └── HashEntry[] → 链表
├── Segment[1]
│ └── HashEntry[] → 链表
├── ...
└── Segment[15] (默认16个Segment)
└── HashEntry[] → 链表
每个Segment独立加锁, 最大并发数 = Segment数量
JDK 8的实现 — CAS + synchronized:
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扩容:
// 初始容量10, 扩容为1.5倍
int newCapacity = oldCapacity + (oldCapacity >> 1);
// 扩容需要Arrays.copyOf, 拷贝整个数组
// 建议: 如果知道大致大小, 指定初始容量
List<String> list = new ArrayList<>(1000);
实际开发建议: - 绝大多数场景用ArrayList(随机访问多,缓存友好) - 仅在频繁头部插入/删除时考虑LinkedList - 作为队列/双端队列时可用LinkedList(实现了Deque接口)
17. HashSet的原理是什么?¶
// 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元素:
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机制是什么?¶
// 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机制:
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缓存)
// 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中集合的新特性?¶
// 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的使用方式:
// 1. 同步实例方法 → 锁住this对象
public synchronized void method() { ... }
// 2. 同步静态方法 → 锁住Class对象
public static synchronized void method() { ... }
// 3. 同步代码块 → 锁住指定对象
synchronized (lockObj) { ... }
底层原理 — Monitor:
每个Java对象都关联一个Monitor(管程/监视器)
synchronized代码块 → monitorenter / monitorexit 指令
synchronized方法 → ACC_SYNCHRONIZED 标志
Monitor结构:
├── _owner: 持有锁的线程
├── _count: 重入次数
├── _EntryList: 等待获取锁的线程队列(阻塞)
└── _WaitSet: 调用wait()后等待的线程集合
锁升级过程(JDK 6优化):
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优化后差不多 | 差不多 |
// 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变量的修改对其他线程立即可见
// 不用volatile → 线程B可能永远看不到flag的变化
// 用volatile → 线程B能立即看到flag的变化
volatile boolean flag = false;
// 线程A
flag = true;
// 线程B
while (!flag) { // 能看到flag变为true
// ...
}
2. 有序性: 禁止指令重排序
// 典型问题: 双检锁单例
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不保证原子性
volatile int count = 0;
// ❌ count++不是原子操作(读→改→写)
// 多线程下count++仍会出问题
count++; // 实际是: temp = count; temp = temp + 1; count = temp;
// ✅ 使用AtomicInteger
AtomicInteger count = new AtomicInteger(0);
count.incrementAndGet();
volatile的内存屏障实现:
写volatile变量:
StoreStore屏障
volatile写
StoreLoad屏障 ← 保证volatile写对后续读可见
读volatile变量:
LoadLoad屏障
volatile读
LoadStore屏障 ← 保证volatile读之后的操作不会重排到读之前
24. 线程池的原理是什么?7个核心参数分别是什么?¶
public ThreadPoolExecutor(
int corePoolSize, // 核心线程数 (即使空闲也不回收)
int maximumPoolSize, // 最大线程数
long keepAliveTime, // 非核心线程的空闲存活时间
TimeUnit unit, // 存活时间单位
BlockingQueue<Runnable> workQueue, // 任务队列
ThreadFactory threadFactory, // 线程工厂(命名等)
RejectedExecutionHandler handler // 拒绝策略
)
线程池工作流程:
提交任务
↓
当前线程数 < corePoolSize?
├── 是 → 创建核心线程执行任务
└── 否 → 任务队列满了?
├── 否 → 加入任务队列等待
└── 是 → 当前线程数 < maximumPoolSize?
├── 是 → 创建非核心线程执行任务
└── 否 → 执行拒绝策略
4种拒绝策略:
| 策略 | 行为 |
|---|---|
| AbortPolicy | 抛出RejectedExecutionException(默认) |
| CallerRunsPolicy | 由提交任务的线程执行(降级) |
| DiscardPolicy | 静默丢弃任务 |
| DiscardOldestPolicy | 丢弃队列中最老的任务,重新提交 |
线程数设置建议:
CPU密集型: corePoolSize = CPU核心数 + 1
IO密集型: corePoolSize = CPU核心数 * 2 (或 CPU核心数 / (1 - IO时间占比))
常用线程池(不推荐直接使用Executors创建):
// ❌ 不推荐(队列无界, 可能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的使用¶
// 异步执行
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实现。
核心结构:
AQS:
├── state (int) : 同步状态
│ - 0: 锁未被持有
│ - >0: 锁被持有(可重入, state=重入次数)
├── exclusiveOwnerThread: 持有锁的线程
└── CLH队列(双向链表): 排队等待获取锁的线程
head ↔ Node(t1) ↔ Node(t2) ↔ Node(t3) ↔ tail
获取锁的过程(以ReentrantLock为例):
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):
// 伪代码
boolean compareAndSwap(V expected, V newValue) {
if (当前值 == expected) {
当前值 = newValue;
return true; // 成功
}
return false; // 失败, 需要重试
}
// CAS是CPU原子指令(cmpxchg), 不需要加锁
Java中的CAS — Unsafe类:
// 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问题:
解决方案 — AtomicStampedReference:
// 加版本号
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原理:
每个Thread对象中有一个ThreadLocalMap:
Thread:
└── ThreadLocalMap:
├── Entry(ThreadLocal_A → value_A)
├── Entry(ThreadLocal_B → value_B)
└── ...
Entry继承WeakReference<ThreadLocal>:
key是ThreadLocal的弱引用
value是强引用
ThreadLocal<User> userContext = new ThreadLocal<>();
// 设置当前线程的值
userContext.set(currentUser);
// 获取当前线程的值
User user = userContext.get();
// 使用完必须remove!
userContext.remove();
内存泄漏问题:
ThreadLocal对象被GC回收后:
Entry的key(WeakReference)变为null
但Entry的value仍然被强引用
Thread → ThreadLocalMap → Entry → value (无法GC!)
如果线程是线程池中的长期线程,ThreadLocalMap不会被清理
→ value永远不会被回收 → 内存泄漏
解决方案:
// 使用完后一定要remove!
try {
threadLocal.set(value);
// 业务逻辑
} finally {
threadLocal.remove(); // 必须清理
}
// 或使用InheritableThreadLocal (父线程传递给子线程)
// 或使用TransmittableThreadLocal (线程池场景)
29. CountDownLatch、CyclicBarrier和Semaphore的区别?¶
CountDownLatch(计数器,一次性):
// 等待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(屏障,可重用):
// N个线程互相等待, 到齐后一起继续
CyclicBarrier barrier = new CyclicBarrier(3, () -> {
System.out.println("所有线程到达栅栏, 一起出发!"); // 到齐后执行
});
for (int i = 0; i < 3; i++) {
executor.execute(() -> {
prepare();
barrier.await(); // 等待其他线程到达
execute(); // 所有线程到齐后一起执行
});
}
Semaphore(信号量,限流):
// 限制并发数量
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 | 无界 | 延迟获取 |
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. 什么是线程安全?如何保证线程安全?¶
线程安全: 多线程环境下代码的执行结果与预期一致。
保证线程安全的方式:
- 互斥同步:synchronized、ReentrantLock
- 非阻塞同步:CAS(Atomic类)
- 无同步方案:
- ThreadLocal(线程隔离)
- 不可变对象(final + String + 不可变集合)
- 栈封闭(局部变量,天然线程安全)
32. 线程的生命周期和状态变化?¶
┌────────────────────────────────────────────┐
│ │
↓ │
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)依赖注入方式:
// 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的完整生命周期:
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种通知类型:
@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
@SpringBootApplication
// 等价于:
@SpringBootConfiguration // = @Configuration, 标识为配置类
@EnableAutoConfiguration // ⭐ 开启自动配置
@ComponentScan // 包扫描
自动配置流程:
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
@Configuration
@ConditionalOnClass(DataSource.class) // classpath有DataSource
@ConditionalOnMissingBean(DataSource.class) // 用户没自定义
@EnableConfigurationProperties(DataSourceProperties.class) // 读取配置
public class DataSourceAutoConfiguration {
@Bean
public DataSource dataSource(DataSourceProperties properties) {
// 根据配置创建数据源
}
}
自定义启用/禁用:
# application.yml
spring:
autoconfigure:
exclude:
- org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration
36. Spring事务管理的原理是什么?¶
声明式事务:
@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 | 不能在事务中调用,否则抛异常 |
事务失效的常见场景:
- 方法不是public:Spring事务基于AOP代理,非public方法不会被代理
- 自调用:同一个类中方法A调方法B,不走代理
- 异常被catch了:事务不知道发生了异常
- 抛出非RuntimeException:默认只回滚RuntimeException
- 数据库引擎不支持事务(如MyISAM)
- Bean未被Spring管理
37. Spring循环依赖是如何解决的?¶
什么是循环依赖:
@Service
public class A {
@Autowired
private B b; // A依赖B
}
@Service
public class B {
@Autowired
private A a; // B依赖A
}
Spring通过三级缓存解决单例Bean的循环依赖:
// DefaultSingletonBeanRegistry
Map<String, Object> singletonObjects; // 一级缓存: 成品Bean
Map<String, Object> earlySingletonObjects; // 二级缓存: 早期Bean(可能是代理)
Map<String, ObjectFactory<?>> singletonFactories; // 三级缓存: Bean工厂
解决过程:
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的请求处理流程是怎样的?¶
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):
@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的原理是什么?¶
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:
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 |
面试答题技巧¶
- JVM题:从内存结构→GC算法→GC收集器→调优,层层递进
- 集合题:讲清底层数据结构和关键方法实现(如HashMap的put)
- 并发题:先讲原理(AQS/CAS),再讲工具类的使用和区别
- Spring题:从IoC/AOP原理出发,结合源码讲解
- 结合实际:讲解OOM排查经历、线程池参数调优等实际案例
最后更新:2025年