📘 本文为「Java 模拟面试总结系列」的第 3 篇,内容由我通过 ChatGPT 模拟问答整理而成,适合用于备战中高级 Java 开发面试。
本篇主题:并发编程与锁机制
# 📌 导航:
[TOC]
# 📚 Java 模拟面试系列目录:
- Java 基础与进阶
- JVM 面试题与知识点详解
- 并发编程与锁机制
- MySQL 与 SQL 优化
- Spring 核心原理
- 系统设计与高并发架构
- Redis 高级特性
- 注册中心与微服务治理
- 项目实战与系统架构设计
# ❓问题 1:你了解 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 关系。
# ❓问题 2: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或加锁保证原子性。
# ❓问题 3:你了解 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(拒绝策略)。 - 执行流程:先创建核心线程处理,满了放队列,队列满了再扩展线程,最后拒绝策略。
# ❓问题 4:简述 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)。 - 自旋锁:短时间等待锁,自旋减少上下文切换。
- 还有读写锁、公平锁等。
# ❓问题 5:请简述 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 |
|---|---|---|
| 保证 | 可见性、禁止重排序 | 原子性、可见性、有序性 |
| 用法 | 只能修饰变量 | 修饰方法或代码块 |
| 性能 | 轻量级 | 重量级(阻塞) |
| 适用 | 状态标识 | 复杂同步操作 |
📖 敬请期待下一篇:《MySQL 与 SQL 优化》