# Java 面试模拟系列 🧠📘

该系列文档基于真实面试准备过程,通过 ChatGPT 辅助问答,对高频 Java 面试题进行整理与答疑。每篇文档聚焦一个主题,覆盖基础、集合、JVM、并发、网络等模块。

序号 文章标题 内容简介
01 Java 模拟面试题(基础篇01) Java 基础知识模拟问答
02 Java 模拟面试题(1) 集合框架、JMM 内存模型等基础进阶题
03 Java 模拟面试题(2) Java 四种引用类型等进阶题
... 🔄 持续更新中 JVM、并发、Spring 等模块

📌 推荐使用场景:

  • 准备面试前的快速复盘
  • 学习 Java 各模块的知识要点
  • 与 ChatGPT 配合自测提问训练

🚀 持续优化中,欢迎 Star & 分享!

# 💡第一部分:Java 基础与进阶

# ❓问题 1:请简述: ArrayListLinkedList 的主要区别?你在什么场景下会选择用 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 ,编译时必须处理(捕获或抛出),如 IOExceptionSQLException
  • Unchecked Exception(运行时异常)
    • 继承自 RuntimeException ,编译时不强制处理,如 NullPointerExceptionIndexOutOfBoundsException
  • Error(错误)
    • JVM 错误,如 OutOfMemoryErrorStackOverflowError ,一般不捕获。

# 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 抛出异常。自定义异常一般继承 ExceptionRuntimeException ,并重写构造方法。

# 🔒 第二部分:并发编程

# ❓问题 3:你了解 Java 的内存模型(JMM)吗?请说一下可见性、有序性和原子性的问题,以及 Java 是如何通过关键字来解决它们的?

# 1️⃣ 原子性

  • 定义:操作是不可再分的最小单位,中间不会被线程切换打断。
  • Java 中如何保证
    • 原始操作如 i++ 不是原子性的
    • 可以使用:
      • synchronized
      • Lock
      • AtomicInteger (底层用 CAS 实现)

# 2️⃣ 可见性

  • 定义:一个线程对共享变量的修改,另一个线程能及时看到
  • Java 提供的可见性保证
    • volatile :写操作会立刻刷新主内存,读操作直接从主内存读
    • synchronized :解锁时会刷新主内存,获取锁会从主内存读取
    • final :在构造函数中安全发布对象

# 3️⃣ 有序性

  • 定义:程序代码的执行顺序是否和编写顺序一致
  • Java 编译器和 CPU 会进行指令重排序(为了优化性能)
  • Java 如何保证有序性
    • volatile :禁止指令重排序
    • synchronized / Lock :建立 happens-before 关系,保证临界区的代码不乱序执行

# 4️⃣答案:

  • 原子性:操作不可分割, synchronizedLockAtomicXXX 保证。
  • 可见性:一个线程修改,其他线程能立即看到, volatilesynchronized 保证。
  • 有序性:执行顺序按代码顺序或 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 BarrierStoreLoad Barrier ,保证刷新主内存
    • 读操作前插入 LoadLoad Barrier ,保证读取的是主内存最新值

# ⚠️ 3. 为什么 volatile 不能保证原子性?

举例:

volatile int count = 0;
public void add() {
    count++;
}

count++ 实际执行分为三步:

  1. 读取 count
  2. +1
  3. 写回 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()
);

# 🎯 小结记忆口诀:

复制编辑核心最大活多久,排队线程谁来造;
满了拒绝交策略,七大参数记得牢。

# ✅答案:

  • 参数: corePoolSizemaximumPoolSizekeepAliveTimeunitworkQueuethreadFactoryhandler (拒绝策略)。
  • 执行流程:先创建核心线程处理,满了放队列,队列满了再扩展线程,最后拒绝策略。

# ❓问题 6:简述 Java 中的锁有哪些?它们各自的特点和适用场景?

# 1️⃣ 悲观锁(Pessimistic Lock)

  • 假设并发冲突很频繁,所以操作之前先加锁,保证线程互斥。
  • Java 中的典型表现:
    • synchronized 关键字(JVM 层实现)
    • ReentrantLock (JDK 提供的显式锁)

# 2️⃣ 乐观锁(Optimistic Lock)

  • 假设并发冲突很少,操作前不加锁,操作后检测数据是否被修改。
  • 典型实现:
    • 数据库中的 version 字段
    • JDK 的 Atomic 类(CAS 机制)

# 3️⃣ 自旋锁(Spin Lock)

  • 线程不会立即阻塞,而是循环等待锁的释放。
  • 适合锁持有时间非常短的场景,减少线程上下文切换开销。
  • Java 中没有直接暴露自旋锁类,但 ReentrantLockAbstractQueuedSynchronizer 内部有自旋机制。

# ✅ 其他锁分类简述:

锁类型 说明 适用场景
偏向锁、轻量级锁 JVM 层面优化,减少无竞争时加锁开销 绝大多数同步场景
读写锁( ReadWriteLock 读多写少,读共享,写独占 高并发读操作
公平锁和非公平锁 公平锁按顺序获得锁,非公平锁效率更高 需要避免线程饥饿时选公平锁

# ✅答案:

  • 悲观锁: synchronizedReentrantLock
  • 乐观锁:版本号机制、 AtomicXXX (CAS)。
  • 自旋锁:短时间等待锁,自旋减少上下文切换。
  • 还有读写锁、公平锁等。

# ❓问题 7:请简述 Java 中 volatilesynchronized 的区别?

#volatilesynchronized 的区别

维度 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 类加载的生命周期阶段

  1. 加载(Loading)
    • 通过类的全限定名获取字节码(二进制流),并生成 Class 对象。
  2. 验证(Verification)
    • 校验字节码的正确性和安全性,防止恶意代码破坏虚拟机安全。
  3. 准备(Preparation)
    • 给类的静态变量分配内存并赋默认值。
  4. 解析(Resolution)
    • 把符号引用转为直接引用(如方法、字段等的内存地址)。
  5. 初始化(Initialization)
    • 执行类的初始化代码,包括静态变量的赋值和静态代码块。
  6. 使用(Using)
    • 类被调用(new 实例、调用静态方法等)。
  7. 卸载(Unloading)
    • 类从内存中卸载,通常由 JVM 垃圾回收机制决定。

# ✅ 双亲委派模型(Parent Delegation Model)

  • 类加载器在加载类时,先将请求委派给父加载器。
  • 流程:
    当前类加载器接到加载请求,先让父类加载器尝试加载。
    如果父加载器找不到,当前加载器才自己尝试加载。

# ✅ 设计目的和好处

  • 防止重复加载同一个类,保证 Java 核心类库的安全。
  • 保证核心类库由启动类加载器加载,避免用户自定义类覆盖核心类。

# ✅简短回答示范:

  • 类加载包括加载、验证、准备、解析、初始化、使用、卸载。
  • 双亲委派模型:请求先委托给父加载器,父加载器找不到才自己加载,保证核心类安全。

# ❓问题 9:谈谈你对 Java 内存泄漏的理解,常见原因和排查方法有哪些?

# ✅ 什么是内存泄漏?

  • 内存泄漏指程序中不再使用的对象仍然被引用,导致垃圾回收器无法回收,从而导致可用内存减少,最终可能导致应用内存溢出(OOM)。

# ✅ Java 中内存泄漏常见原因

  1. 长生命周期对象持有短生命周期对象引用
    例如:
    • 静态集合( static List )不断往里添加对象,但没有清理;
    • 线程池、缓存持有对象,导致对象无法被回收。
  2. 监听器或回调未注销
    • 注册了事件监听器或观察者,但没有取消,导致对象无法被回收。
  3. 内部类和匿名类持有外部类引用
    • 导致外部类对象无法回收。
  4. 数据库连接、IO 流未关闭
    • 资源没有释放,导致内存和资源泄漏。
  5. 线程未正确结束
    • 线程还活着,线程内持有对象,无法回收。

# ✅ 内存泄漏的排查方法

  1. 使用内存分析工具
    • VisualVMEclipse MATJProfilerYourKit 等。
  2. 堆转储(Heap Dump)分析
    • 生成堆快照,分析哪些对象占用大量内存,找到引用链。
  3. 代码审查
    • 检查静态变量、集合、缓存的使用。
  4. 增加日志和监控
    • 监控 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 的声明式事务吗?请回答:**

  1. @Transactional 注解一般写在什么地方?
  2. 默认情况下,哪些异常会触发事务回滚?
  3. 如果我希望遇到某个自定义异常不回滚,应该怎么做?

#答案:

  1. @Transactional 最推荐放在 Service 层的方法上,因为:

    • 控制事务粒度更细;

    • 避免类中某些非事务方法被误标;

    • 类上标注表示所有 public 方法都带事务,但无法精细控制。

  2. 默认运行时异常和 Error 回滚,受检异常不回滚。

  3. 如果想让事务遇到某个自定义异常不回滚,可以在 @Transactional 注解中用 noRollbackFor 属性指定该异常的类,比如:

    @Transactional(noRollbackFor = MyCustomException.class)

    这样即使抛出 MyCustomException ,事务也不会回滚。