# Java 面试模拟系列 🧠📘
该系列文档基于真实面试准备过程,通过 ChatGPT 辅助问答,对高频 Java 面试题进行整理与答疑。每篇文档聚焦一个主题,覆盖基础、集合、JVM、并发、网络等模块。
序号 文章标题 内容简介 01 Java 模拟面试题(基础篇01) Java 基础知识模拟问答 02 Java 模拟面试题(1) 集合框架、JMM 内存模型等基础进阶题 03 Java 模拟面试题(2) Java 四种引用类型等进阶题 ... 🔄 持续更新中 JVM、并发、Spring 等模块
📌 推荐使用场景:
- 准备面试前的快速复盘
- 学习 Java 各模块的知识要点
- 与 ChatGPT 配合自测提问训练
🚀 持续优化中,欢迎 Star & 分享!
# 💡第一部分:Java 基础与进阶
# ❓问题 1:请简述: ArrayList 和 LinkedList 的主要区别?你在什么场景下会选择用 LinkedList ?
答案:
- ArrayList 底层是动态数组,支持快速随机访问(O (1)),插入删除慢(需要移动元素)。
- LinkedList 是双向链表,访问慢(O (n)),插入删除快(只修改指针)。
- 两者都非线程安全。
- LinkedList 适合频繁在头尾插入删除,或用作双端队列(Deque),但大部分场景用 ArrayList。ArrayDeque 通常比 LinkedList 性能更好。
| 对比维度 | ArrayList | LinkedList |
|---|---|---|
| 底层结构 | 动态数组 Object[] |
双向链表 |
| 访问性能 | O(1) 随机访问快 | O (n) 顺序遍历 |
| 插入 / 删除 | 末尾插入快,中间插入慢(需移动元素) | 中间插入删除快(修改前后指针) |
| 内存占用 | 紧凑,节省空间 | 每个节点额外维护 prev/next,内存开销大 |
| 线程安全 | 都是非线程安全,需外部加锁 | 同上 |
| 使用场景 | 读多写少 | 写多、频繁插入删除中间节点(但几乎不用) |
# ❓问题 2:Java 异常处理机制?
# 1️⃣ 异常分类:
- Checked Exception(受检异常)
- 继承自
Exception,编译时必须处理(捕获或抛出),如IOException、SQLException。
- 继承自
- Unchecked Exception(运行时异常)
- 继承自
RuntimeException,编译时不强制处理,如NullPointerException、IndexOutOfBoundsException。
- 继承自
- Error(错误)
- JVM 错误,如
OutOfMemoryError、StackOverflowError,一般不捕获。
- JVM 错误,如
# 2️⃣ 异常捕获和抛出:
- 使用
try-catch-finally捕获异常,finally块执行清理操作,无论是否异常都会执行。 - 使用
throw抛出异常实例,使用throws声明方法可能抛出的异常。
示例:
try { | |
// 可能抛出异常的代码 | |
} catch (IOException e) { | |
e.printStackTrace(); | |
} finally { | |
// 清理操作 | |
} |
# 3️⃣ 自定义异常:
- 一般继承
Exception(受检异常)或RuntimeException(非受检异常) - 推荐添加构造器方便传递异常信息
示例:
public class MyException extends Exception { | |
public MyException(String message) { | |
super(message); | |
} | |
} |
# ✅ 总结:
- 异常分受检异常(Exception)、运行时异常(RuntimeException)和错误(Error)。
- 用 try-catch-finally 捕获,throw 抛出。
- 自定义异常继承 Exception 或 RuntimeException。
# ✅ 简短示范回答
Java 异常分为三类:受检异常(必须捕获或声明抛出)、运行时异常(可选捕获)、错误(一般不捕获)。程序中通过
try-catch-finally捕获异常,throw抛出异常。自定义异常一般继承Exception或RuntimeException,并重写构造方法。
# 🔒 第二部分:并发编程
# ❓问题 3:你了解 Java 的内存模型(JMM)吗?请说一下可见性、有序性和原子性的问题,以及 Java 是如何通过关键字来解决它们的?
# 1️⃣ 原子性
- 定义:操作是不可再分的最小单位,中间不会被线程切换打断。
- Java 中如何保证:
- 原始操作如
i++不是原子性的 - 可以使用:
synchronizedLockAtomicInteger(底层用 CAS 实现)
- 原始操作如
# 2️⃣ 可见性
- 定义:一个线程对共享变量的修改,另一个线程能及时看到。
- Java 提供的可见性保证:
volatile:写操作会立刻刷新主内存,读操作直接从主内存读synchronized:解锁时会刷新主内存,获取锁会从主内存读取final:在构造函数中安全发布对象
# 3️⃣ 有序性
- 定义:程序代码的执行顺序是否和编写顺序一致
- Java 编译器和 CPU 会进行指令重排序(为了优化性能)
- Java 如何保证有序性:
volatile:禁止指令重排序synchronized/Lock:建立 happens-before 关系,保证临界区的代码不乱序执行
# 4️⃣答案:
- 原子性:操作不可分割,
synchronized、Lock、AtomicXXX保证。 - 可见性:一个线程修改,其他线程能立即看到,
volatile和synchronized保证。 - 有序性:执行顺序按代码顺序或 JMM 保证,
volatile禁止指令重排序,synchronized建立 happens-before 关系。
# ❓问题 4:volatile 的底层原理及它能保证原子性吗?如果不能,那你遇到 volatile i++ 这样的代码要怎么改?
# 🔍 1. volatile 的作用
- 保证可见性:对变量的写操作,会立刻刷新到主内存,其他线程读操作一定从主内存读取。
- 禁止指令重排序:读写操作不会被 JVM 或 CPU 编译器重排。
# 🧠 2. volatile 的底层原理
在 JDK 层和 JVM 层, volatile 的底层实现分为:
# ✅ Java 层:
- 被编译为特殊的字节码指令:
volatile修饰字段在字节码中有volatile标记。
# ✅ JVM 层(对应 CPU 内存模型):
- 会在底层插入 内存屏障(Memory Barrier) 指令:
- 写操作后插入
StoreStore Barrier和StoreLoad Barrier,保证刷新主内存 - 读操作前插入
LoadLoad Barrier,保证读取的是主内存最新值
- 写操作后插入
# ⚠️ 3. 为什么 volatile 不能保证原子性?
举例:
volatile int count = 0; | |
public void add() { | |
count++; | |
} |
count++ 实际执行分为三步:
- 读取 count
- +1
- 写回 count
多线程并发执行时,这三个步骤不是原子的,会导致丢数据。
# ✅ 4. 正确做法?
使用:
AtomicInteger count = new AtomicInteger(); | |
public void add() { | |
count.incrementAndGet(); // 原子自增 | |
} |
底层用的是 CAS + volatile 变量 实现原子性 + 可见性。
# ✅ 答案:
volatile通过内存屏障保证可见性和禁止重排序。- 不能保证复合操作(如
i++)的原子性。 - 需用
AtomicInteger或加锁保证原子性。
# ❓问题 5:你了解 Java 中的线程池吗?请简要说明线程池的 7 个参数含义,以及任务提交后的执行流程。
# ✅ 一、线程池的 7 个核心参数(来自 ThreadPoolExecutor 构造函数)
ThreadPoolExecutor executor = new ThreadPoolExecutor( | |
corePoolSize, // 核心线程数 | |
maximumPoolSize, // 最大线程数 | |
keepAliveTime, // 非核心线程存活时间 | |
unit, // 存活时间单位 | |
workQueue, // 阻塞队列 | |
threadFactory, // 线程工厂(创建线程) | |
handler // 拒绝策略 | |
); |
| 参数名 | 含义 |
|---|---|
corePoolSize |
核心线程数,始终保留,哪怕空闲 |
maximumPoolSize |
最大线程数 |
keepAliveTime |
非核心线程的最大空闲时间,超过就销毁 |
unit |
上面时间的单位(秒、毫秒等) |
workQueue |
用来存放等待执行的任务 |
threadFactory |
用于创建新线程,可自定义线程名等 |
handler |
拒绝策略,线程和队列都满后触发 |
# ✅ 二、任务提交到线程池后会发生什么(执行流程)
1. 当前线程数 < corePoolSize: | |
→ 直接创建新线程处理任务 | |
2. 当前线程数 ≥ corePoolSize: | |
→ 尝试放入工作队列(workQueue) | |
3. 工作队列满了: | |
→ 如果线程总数 < maximumPoolSize,则创建新线程执行任务 | |
4. 如果线程总数 = maximumPoolSize 且队列也满了: | |
→ 执行拒绝策略(handler) |
# 🔧 常见阻塞队列(workQueue)类型
| 队列类型 | 特点 |
|---|---|
ArrayBlockingQueue |
有界队列,FIFO |
LinkedBlockingQueue |
无界队列(可指定大小),吞吐高 |
SynchronousQueue |
不存储任务,来了就必须被线程处理 |
PriorityBlockingQueue |
按优先级排序处理任务 |
# 🔧 拒绝策略(handler)
RejectedExecutionHandler handler = | |
new ThreadPoolExecutor.AbortPolicy(); // 默认,抛异常 | |
// 其他策略: | |
CallerRunsPolicy // 由调用线程执行(慢但不丢) | |
DiscardPolicy // 直接丢弃任务 | |
DiscardOldestPolicy // 丢最旧的,尝试提交新任务 |
# ✅ 实际应用举例(建议你在项目中也封装一份)
ThreadPoolExecutor pool = new ThreadPoolExecutor( | |
4, 8, 60, TimeUnit.SECONDS, | |
new ArrayBlockingQueue<>(100), | |
Executors.defaultThreadFactory(), | |
new ThreadPoolExecutor.AbortPolicy() | |
); |
# 🎯 小结记忆口诀:
复制编辑核心最大活多久,排队线程谁来造;
满了拒绝交策略,七大参数记得牢。
# ✅答案:
- 参数:
corePoolSize、maximumPoolSize、keepAliveTime、unit、workQueue、threadFactory、handler(拒绝策略)。 - 执行流程:先创建核心线程处理,满了放队列,队列满了再扩展线程,最后拒绝策略。
# ❓问题 6:简述 Java 中的锁有哪些?它们各自的特点和适用场景?
# 1️⃣ 悲观锁(Pessimistic Lock)
- 假设并发冲突很频繁,所以操作之前先加锁,保证线程互斥。
- Java 中的典型表现:
synchronized关键字(JVM 层实现)ReentrantLock(JDK 提供的显式锁)
# 2️⃣ 乐观锁(Optimistic Lock)
- 假设并发冲突很少,操作前不加锁,操作后检测数据是否被修改。
- 典型实现:
- 数据库中的
version字段 - JDK 的
Atomic类(CAS 机制)
- 数据库中的
# 3️⃣ 自旋锁(Spin Lock)
- 线程不会立即阻塞,而是循环等待锁的释放。
- 适合锁持有时间非常短的场景,减少线程上下文切换开销。
- Java 中没有直接暴露自旋锁类,但
ReentrantLock和AbstractQueuedSynchronizer内部有自旋机制。
# ✅ 其他锁分类简述:
| 锁类型 | 说明 | 适用场景 |
|---|---|---|
| 偏向锁、轻量级锁 | JVM 层面优化,减少无竞争时加锁开销 | 绝大多数同步场景 |
读写锁( ReadWriteLock ) |
读多写少,读共享,写独占 | 高并发读操作 |
| 公平锁和非公平锁 | 公平锁按顺序获得锁,非公平锁效率更高 | 需要避免线程饥饿时选公平锁 |
# ✅答案:
- 悲观锁:
synchronized、ReentrantLock。 - 乐观锁:版本号机制、
AtomicXXX(CAS)。 - 自旋锁:短时间等待锁,自旋减少上下文切换。
- 还有读写锁、公平锁等。
# ❓问题 7:请简述 Java 中 volatile 和 synchronized 的区别?
# ✅ volatile 和 synchronized 的区别
| 维度 | volatile | synchronized |
|---|---|---|
| 作用 | 保证变量的可见性和禁止指令重排序 | 保证可见性、原子性和有序性 |
| 使用范围 | 只能修饰变量 | 可修饰代码块、方法,锁对象不同(对象锁 / 类锁) |
| 锁机制 | 不加锁,不阻塞线程 | 通过获取对象监视器实现阻塞和同步 |
| 性能开销 | 轻量级,性能开销小 | 相对较重,涉及线程阻塞和唤醒 |
| 原子性 | 不保证原子性 | 保证操作的原子性 |
| 适用场景 | 变量状态标志、状态变化通知 | 需要保证复合操作线程安全,临界区代码同步 |
# ✅ 简单示例
-
volatile
private volatile boolean flag = true;
用于多个线程之间快速通知状态变化。
-
synchronized
public synchronized void increment() {
count++;
}用于保证代码块执行的互斥,防止并发数据错误。
# ✅ 额外补充
volatile不会阻塞线程,也不保证多个操作的整体原子性(比如count++)。synchronized会阻塞,且能保证执行代码块期间线程独占锁。
# ✅ 总结
volatile用于保证共享变量的可见性和防止指令重排序,适合轻量级的状态标识,不保证原子性;而synchronized是重量级锁,保证代码块的互斥执行,确保了操作的原子性和可见性,但性能开销较大。
| 特性 | volatile | synchronized |
|---|---|---|
| 保证 | 可见性、禁止重排序 | 原子性、可见性、有序性 |
| 用法 | 只能修饰变量 | 修饰方法或代码块 |
| 性能 | 轻量级 | 重量级(阻塞) |
| 适用 | 状态标识 | 复杂同步操作 |
# 🧠 第二部分:JVM
# ❓问题 8:Java 中的类加载机制是怎样的?双亲委派模型是什么?
# ✅ Java 类加载的生命周期阶段
- 加载(Loading)
- 通过类的全限定名获取字节码(二进制流),并生成
Class对象。
- 通过类的全限定名获取字节码(二进制流),并生成
- 验证(Verification)
- 校验字节码的正确性和安全性,防止恶意代码破坏虚拟机安全。
- 准备(Preparation)
- 给类的静态变量分配内存并赋默认值。
- 解析(Resolution)
- 把符号引用转为直接引用(如方法、字段等的内存地址)。
- 初始化(Initialization)
- 执行类的初始化代码,包括静态变量的赋值和静态代码块。
- 使用(Using)
- 类被调用(new 实例、调用静态方法等)。
- 卸载(Unloading)
- 类从内存中卸载,通常由 JVM 垃圾回收机制决定。
# ✅ 双亲委派模型(Parent Delegation Model)
- 类加载器在加载类时,先将请求委派给父加载器。
- 流程:
当前类加载器接到加载请求,先让父类加载器尝试加载。
如果父加载器找不到,当前加载器才自己尝试加载。
# ✅ 设计目的和好处
- 防止重复加载同一个类,保证 Java 核心类库的安全。
- 保证核心类库由启动类加载器加载,避免用户自定义类覆盖核心类。
# ✅简短回答示范:
- 类加载包括加载、验证、准备、解析、初始化、使用、卸载。
- 双亲委派模型:请求先委托给父加载器,父加载器找不到才自己加载,保证核心类安全。
# ❓问题 9:谈谈你对 Java 内存泄漏的理解,常见原因和排查方法有哪些?
# ✅ 什么是内存泄漏?
- 内存泄漏指程序中不再使用的对象仍然被引用,导致垃圾回收器无法回收,从而导致可用内存减少,最终可能导致应用内存溢出(OOM)。
# ✅ Java 中内存泄漏常见原因
- 长生命周期对象持有短生命周期对象引用
例如:- 静态集合(
static List)不断往里添加对象,但没有清理; - 线程池、缓存持有对象,导致对象无法被回收。
- 静态集合(
- 监听器或回调未注销
- 注册了事件监听器或观察者,但没有取消,导致对象无法被回收。
- 内部类和匿名类持有外部类引用
- 导致外部类对象无法回收。
- 数据库连接、IO 流未关闭
- 资源没有释放,导致内存和资源泄漏。
- 线程未正确结束
- 线程还活着,线程内持有对象,无法回收。
# ✅ 内存泄漏的排查方法
- 使用内存分析工具
- 如
VisualVM、Eclipse MAT、JProfiler、YourKit等。
- 如
- 堆转储(Heap Dump)分析
- 生成堆快照,分析哪些对象占用大量内存,找到引用链。
- 代码审查
- 检查静态变量、集合、缓存的使用。
- 增加日志和监控
- 监控 JVM 内存使用、GC 日志。
# ✅总结:
- 内存泄漏是无用对象被引用无法回收。
- 常见原因:静态集合无清理,监听器未注销,线程未结束等。
- 排查用 VisualVM、MAT、代码审查。
# ✅ 简短示范回答
Java 内存泄漏是指不再使用的对象被持续引用,导致垃圾回收无法回收,最终可能发生内存溢出。常见原因包括静态集合无限制增长、监听器未注销、线程未关闭等。排查方法有使用 VisualVM、MAT 等工具分析堆内存,结合代码审查定位问题。
# ❓问题 10:Java 垃圾回收原理和常见回收器?
# ✅ Java 垃圾回收(GC)基本原理
- Java 的垃圾回收机制自动管理内存,主要目的是回收不再使用的对象,释放内存,避免内存泄漏和溢出。
- JVM 堆内存分为年轻代(Young Generation)** 和 ** 老年代(Old Generation)。
- 新生代采用复制算法(Copying),老年代采用标记 - 整理算法(Mark-Compact)。
- 垃圾回收的核心思想是可达性分析:从 GC Roots 开始,所有无法被访问的对象视为垃圾。
# ✅ 常见的垃圾回收器及适用场景
| 垃圾回收器 | 简介 | 适用场景 |
|---|---|---|
| Serial(串行收集器) | 单线程执行,简单高效,停顿时间长 | 单核 CPU,客户端应用,低内存环境 |
| Parallel(并行收集器) | 多线程执行,注重吞吐量,停顿较短 | 多核 CPU,后台批处理,吞吐量优先 |
| CMS(并发标记清除) | 并发回收,停顿短,但 CPU 占用高 | 需要低停顿的应用,如 Web 服务器 |
| G1(Garbage-First) | 以区域为单位回收,低延迟、可预测停顿 | 多核大内存服务器,需响应时间可控的场景 |
| ZGC / Shenandoah | 低延迟、几乎不暂停的垃圾回收器 | 大内存超低延迟应用(JDK 11+ / 12+) |
# ✅ 简短答法示范
Java GC 自动管理内存,主要通过可达性分析判断对象是否可回收。堆内存分为年轻代和老年代,分别用不同算法回收。常见回收器有串行、并行、CMS 和 G1,选择时根据应用对吞吐量和延迟的需求权衡。
# ✅答案:
- GC 自动回收无用对象,堆分年轻代和老年代。
- 常用回收器:Serial、Parallel、CMS、G1、ZGC 等。
- 选择依据应用延迟和吞吐需求。
# 🌱 第四部分:Spring 核心原理
# ❓问题 11:你了解 Spring 的声明式事务吗?请回答:**
@Transactional注解一般写在什么地方?- 默认情况下,哪些异常会触发事务回滚?
- 如果我希望遇到某个自定义异常不回滚,应该怎么做?
# ✅答案:
-
@Transactional最推荐放在Service层的方法上,因为:-
控制事务粒度更细;
-
避免类中某些非事务方法被误标;
-
类上标注表示所有
public方法都带事务,但无法精细控制。
-
-
默认运行时异常和 Error 回滚,受检异常不回滚。
-
如果想让事务遇到某个自定义异常不回滚,可以在
@Transactional注解中用noRollbackFor属性指定该异常的类,比如:@Transactional(noRollbackFor = MyCustomException.class)
这样即使抛出
MyCustomException,事务也不会回滚。