Java 并发编程面试核心知识点精要
Java 并发编程面试核心知识点精要
🔥 一、notify () 和 notifyAll () 的区别(重点)
✅ 核心区别
| 特性 | notify() | notifyAll() |
|---|---|---|
| 唤醒数量 | 随机唤醒 一个 等待线程 | 唤醒 所有 等待线程 |
| 唤醒方式 | JVM 随机选择(不可控) | 所有在等待队列中的线程都被唤醒 |
| 资源竞争 | 被唤醒线程进入锁池争锁 | 所有被唤醒线程进入锁池共同争锁 |
| 使用安全性 | 较低,可能 “线程饥饿” 或漏唤醒 | 更安全,推荐复杂场景 |
| 性能开销 | 小(仅唤醒一个) | 相对较大(多线程争锁) |
🧠 关键概念解析
1. 等待池(Wait Set / 等待队列)
当线程调用对象的 wait() 方法时,会发生以下操作:
- 释放该对象的 内置锁(monitor 锁)
- 进入该对象的 等待池
- 等待池中的线程 不再参与锁的竞争
- 只能通过
notify()或notifyAll()被唤醒 - 唤醒后不会立即执行,需转移到 锁池 重新争锁
2. 锁池(Entry Set / 阻塞队列)
- 所有试图获取同步锁但失败的线程会进入锁池
- 只有获得对象锁的线程才能执行
synchronized代码块 - 每个对象的锁同一时间只能被一个线程持有
- 被
notify()唤醒的线程必须重新竞争锁才能继续执行
💡 使用建议(面试加分项)
| 场景 | 推荐使用 |
|---|---|
| 所有等待线程等待同一条件,且仅需一个线程处理(如单消费者场景) | notify() |
| 多个线程等待不同条件,或无法确定哪个线程满足当前条件 | notifyAll () ✅(推荐) |
| 对性能要求极高 | 根据场景谨慎选择 |
🧪 经典示例:生产者 - 消费者模型
public class BoundedBuffer {
private final Object lock = new Object();
private final List<Object> items = new ArrayList<>();
private static final int MAX_SIZE = 10;
// 生产者
public void produce(Object item) throws InterruptedException {
synchronized (lock) {
while (items.size() == MAX_SIZE) {
lock.wait(); // 缓冲区满,进入等待池
}
items.add(item);
lock.notifyAll(); // 唤醒所有消费者(可能存在多个)
}
}
// 消费者
public Object consume() throws InterruptedException {
synchronized (lock) {
while (items.isEmpty()) {
lock.wait(); // 缓冲区空,进入等待池
}
Object item = items.remove(0);
lock.notifyAll(); // 唤醒所有生产者
return item;
}
}
}
关键说明:若用 notify(),可能唤醒另一个生产者线程(而非消费者),导致消费者持续等待,引发 “信号丢失”。
📝 面试回答模板
notify() 会随机唤醒一个等待线程,具体唤醒哪个由 JVM 决定,可能因 “唤醒错误线程” 导致 “线程饥饿” 或信号丢失;notifyAll() 会唤醒所有等待线程,这些线程进入锁池重新争锁,虽性能开销略大,但能避免上述问题,在多条件等待场景中更安全可靠,是更推荐的选择。
🔥 二、如何选择锁的范围(重点)
✅ 核心原则
只对真正需要同步的代码加锁,避免不必要的同步操作 —— 锁的本质是串行化访问共享资源,过度同步会降低并发性能。
🔍 为什么要控制锁的范围?
- 锁范围越大 → 线程持有锁时间越长 → 其他线程等待越久 → 并发度下降
- 锁范围越小 → 持有锁时间越短 → 更多线程快速获取锁 → 吞吐量提升
🧱 如何合理选择锁的范围?
✅ 正确做法:细粒度加锁
public class Counter {
private int count = 0;
// ❌ 错误示范:锁范围过大(包含非同步操作)
public void badMethod() throws InterruptedException {
synchronized (this) {
System.out.println("Doing some unrelated work..."); // 无共享状态操作
Thread.sleep(100); // 耗时操作
count++; // 仅这一句需要同步
}
}
// ✅ 正确示范:缩小锁范围
public void goodMethod() throws InterruptedException {
// 非临界区代码(不加锁)
System.out.println("Doing some unrelated work...");
Thread.sleep(100);
// 仅对共享变量操作加锁
synchronized (this) {
count++;
}
}
}
💡 小技巧:将耗时操作移出同步块
- 禁止将 I/O 操作(文件读写、网络请求)、日志打印、Thread.sleep () 等耗时操作放入
synchronized块内 - 否则会导致其他线程长时间阻塞,严重降低并发性能
🛠️ 进阶策略:使用局部同步对象
避免直接锁定 this(整个对象),改用专门的锁对象,进一步控制锁粒度:
public class BankAccount {
// 专用锁对象(private final 保证不可变,避免被外部修改)
private final Object lock = new Object();
private double balance;
public void transfer(double amount) {
synchronized (lock) {
this.balance += amount;
}
}
}
优势:
- 防止外部代码恶意锁定
this对象 - 更灵活地控制锁的作用域(如多锁分离场景)
🔄 根据场景选择合适的锁机制
| 场景 | 推荐方式 |
|---|---|
| 简单原子操作(如 i++) | 使用 AtomicInteger 等 CAS 类(无锁化) |
| 读多写少 | 使用 ReentrantReadWriteLock 或 StampedLock |
| 高并发争用严重 | 使用 LongAdder、ConcurrentHashMap 等分段锁结构 |
| 必须使用 synchronized | 缩小同步块,避免嵌套锁 |
⚖️ 经典权衡案例:缓存加载
❌ 错误示例(耗时操作在同步块内)
public String getData(String key) {
synchronized (cache) {
String value = cache.get(key);
if (value == null) {
value = loadFromDB(); // 耗时数据库操作(不应在同步块内)
cache.put(key, value);
}
return value;
}
}
✅ 改进方案:双重检查 + 局部加锁
private final Map<String, String> cache = new ConcurrentHashMap<>();
private final Object lock = new Object();
public String getData(String key) {
// 第一次检查(无锁,快速返回已存在数据)
String value = cache.get(key);
if (value == null) {
// 局部加锁(仅缓存未命中时同步)
synchronized (lock) {
// 第二次检查(防止多线程重复加载)
value = cache.get(key);
if (value == null) {
value = loadFromDB(); // 耗时操作移出锁外(仅同步时执行一次)
cache.put(key, value);
}
}
}
return value;
}
📝 面试回答模板
选择锁范围的核心原则是 “最小粒度同步”:只对操作共享资源的临界区代码加锁,将非同步操作(如日志、I/O)和耗时操作(如数据库查询)移出同步块,减少线程持有锁的时间;同时可通过 “专用锁对象” 替代 this 锁定,避免外部干扰;复杂场景下,还可结合无锁类(如 Atomic 系列)或并发容器(如 ConcurrentHashMap)进一步优化,平衡线程安全与并发性能。
🔥 三、如何停止一个线程(重点)
🚫 不推荐:Thread.stop ()
Thread.stop()已被 标记为废弃(deprecated)- 风险:立即终止线程并释放所有锁,可能导致:
- 数据不一致(如对象状态未完成更新)
- 锁未正确释放,引发死锁或资源泄漏
✅ 正确方式一:使用中断机制(interrupt)—— 推荐标准做法
Java 提供 “中断协议”,通过 “中断标志位” 实现线程优雅退出:
public class InterruptExample implements Runnable {
@Override
public void run() {
// 定期检查中断状态
while (!Thread.currentThread().isInterrupted()) {
try {
// 阻塞方法(如 sleep/wait/join)会响应中断
Thread.sleep(1000);
System.out.println("执行任务中...");
} catch (InterruptedException e) {
// 关键:捕获异常后,中断状态会被清除,需重新设置
Thread.currentThread().interrupt();
System.out.println("接收到中断信号,准备退出");
break; // 退出循环,执行清理
}
}
// 资源清理(如关闭连接、释放文件)
cleanup();
}
private void cleanup() {
System.out.println("执行资源清理");
}
// 外部触发中断
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new InterruptExample());
thread.start();
Thread.sleep(3000); // 运行3秒后触发中断
thread.interrupt();
}
}
📌 关键点
interrupt()不直接停止线程,仅设置 “中断标志位”- 阻塞方法(
sleep/wait/join)会触发InterruptedException,并 自动清除中断状态 - 捕获异常后必须 重新调用 interrupt (),确保上层代码感知中断请求
✅ 正确方式二:使用 volatile 标志位控制(配合中断更佳)
适用于自定义循环任务场景,通过 volatile 保证变量可见性:
public class VolatileStopExample implements Runnable {
// volatile 保证多线程间可见性
private volatile boolean running = true;
@Override
public void run() {
// 以标志位控制循环
while (running) {
System.out.println("执行任务中...");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
}
cleanup(); // 资源清理
}
// 外部调用此方法停止线程
public void shutdown() {
running = false;
}
private void cleanup() {
System.out.println("执行资源清理");
}
}
📌 为什么用 volatile?
- 防止线程因 “CPU 缓存” 导致无法及时感知
running的变化 - 保证多线程间
running变量的可见性(写操作立即刷新到主内存,读操作从主内存读取)
✅ 正确方式三:线程池管理 —— 大量线程场景的标准做法
若使用线程池(如 ExecutorService),需通过线程池 API 关闭,而非直接操作线程:
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
public class ThreadPoolStopExample {
public static void main(String[] args) throws InterruptedException {
// 创建线程池
ExecutorService executor = Executors.newFixedThreadPool(5);
// 提交任务
for (int i = 0; i < 10; i++) {
executor.submit(() -> System.out.println("执行线程池任务"));
}
// 方式1:平滑关闭(不接受新任务,等待已提交任务完成)
executor.shutdown();
// 方式2:等待超时(5秒内未完成则强制处理)
if (!executor.awaitTermination(5, TimeUnit.SECONDS)) {
// 方式3:立即中断所有正在执行的任务
List<Runnable> droppedTasks = executor.shutdownNow();
System.out.println("未执行的任务数:" + droppedTasks.size());
}
}
}
📌 最佳实践
- 先调用
shutdown()让任务自然结束 - 用
awaitTermination(timeout)等待完成 - 超时未结束时,再调用
shutdownNow()强制中断
🔔 注意事项(面试加分项)
| 注意点 | 说明 |
|---|---|
| 处理 InterruptedException | 不可忽略该异常,需恢复中断状态或向上抛出,遵循中断语义 |
| 资源清理工作 | 线程退出前必须关闭文件、网络连接、数据库连接等资源 |
| 避免暴力终止 | 即使程序退出,也应让线程自行清理,防止数据损坏 |
📝 面试回答模板
停止线程的核心是 “优雅退出”,不推荐使用废弃的 stop() 方法;推荐两种标准方式:一是使用 中断机制,通过 interrupt() 发送中断信号,线程内部通过 isInterrupted() 检查状态,捕获 InterruptedException 后需重新设置中断状态;二是使用 volatile 标志位,通过可见性变量控制循环退出;若使用线程池,需调用 shutdown() + awaitTermination() 平滑关闭,超时后再用 shutdownNow() 强制中断,确保资源不泄漏。
🔥 四、synchronized 和 Lock 的区别(重点)
✅ 核心区别对比
| 对比维度 | synchronized | Lock(如 ReentrantLock) |
|---|---|---|
| 实现层次 | JVM 关键字(底层由 JVM 实现) | JDK 接口(java.util.concurrent.locks) |
| 锁类型 | 隐式锁(自动获取 / 释放) | 显式锁(需手动调用 lock ()/unlock ()) |
| 异常处理 | 异常时自动释放锁,安全性高 | 异常不自动释放,需在 finally 中手动释放 |
| 中断响应 | 不支持(线程阻塞后无法中断) | 支持(lockInterruptibly () 可中断等待) |
| 尝试获取锁 | 无法尝试,只能阻塞等待 | 支持 tryLock (),可设置超时时间 |
| 读写分离 | 不支持 | 支持 ReadWriteLock,提升读并发 |
| 条件变量 | 依赖 wait ()/notify ()/notifyAll ()(仅 1 个等待队列) | 依赖 Condition(支持多个等待队列) |
| 公平性 | 仅非公平锁(默认,不可更改) | 可选择公平 / 非公平锁(构造参数控制) |
| 性能表现 | 低并发时性能好,高并发时低于 Lock | 高并发场景性能更优,适合复杂控制 |
✅ 核心特性详解
1. 自动 vs 手动释放锁
// synchronized:自动释放(异常也会释放)
public synchronized void syncMethod() {
throw new RuntimeException("异常测试"); // 锁会自动释放
}
// Lock:必须手动释放(依赖 try-finally)
public void lockMethod() {
Lock lock = new ReentrantLock();
lock.lock(); // 手动加锁
try {
// 业务逻辑
} finally {
lock.unlock(); // 必须在 finally 中释放,防止死锁
}
}
2. 中断支持
public void interruptibleLock() throws InterruptedException {
Lock lock = new ReentrantLock();
// 支持中断的加锁方式:线程被中断时抛出 InterruptedException
lock.lockInterruptibly();
try {
// 业务逻辑
} finally {
lock.unlock();
}
}
synchronized:线程阻塞后无法响应中断,只能一直等待Lock:通过lockInterruptibly()支持中断,适合 “需要取消等待” 的场景(如超时控制)
3. 尝试获取锁(tryLock)
public void tryLockExample() throws InterruptedException {
Lock lock = new ReentrantLock();
// 尝试获取锁,3秒超时后返回 false
if (lock.tryLock(3, TimeUnit.SECONDS)) {
try {
System.out.println("获取锁成功");
} finally {
lock.unlock();
}
} else {
System.out.println("获取锁失败,执行降级逻辑");
}
}
- 避免 “无限等待”,可在获取失败时执行降级策略(如返回默认值、记录日志)
4. 读写锁支持(ReadWriteLock)
public class ReadWriteLockExample {
// 读写锁:读锁共享,写锁独占
private final ReadWriteLock rwLock = new ReentrantReadWriteLock();
private final Lock readLock = rwLock.readLock(); // 读锁
private final Lock writeLock = rwLock.writeLock(); // 写锁
private int data;
// 读操作(共享,多线程可同时执行)
public int readData() {
readLock.lock();
try {
return data;
} finally {
readLock.unlock();
}
}
// 写操作(独占,仅一个线程可执行)
public void writeData(int newData) {
writeLock.lock();
try {
data = newData;
} finally {
writeLock.unlock();
}
}
}
synchronized无读写分离,所有访问互斥;ReadWriteLock在读多写少场景下可大幅提升并发性能
5. Condition 实现精准唤醒
public class ConditionExample {
private final Lock lock = new ReentrantLock();
// 多个条件队列(如“非满”和“非空”)
private final Condition notFull = lock.newCondition();
private final Condition notEmpty = lock.newCondition();
private final Object[] buffer = new Object[10];
private int count;
// 生产者:等待“非满”
public void produce(Object item) throws InterruptedException {
lock.lock();
try {
while (count == buffer.length) {
notFull.await(); // 进入“非满”等待队列
}
buffer[count++] = item;
notEmpty.signal(); // 唤醒“非空”等待队列的消费者
} finally {
lock.unlock();
}
}
// 消费者:等待“非空”
public Object consume() throws InterruptedException {
lock.lock();
try {
while (count == 0) {
notEmpty.await(); // 进入“非空”等待队列
}
Object item = buffer[--count];
notFull.signal(); // 唤醒“非满”等待队列的生产者
} finally {
lock.unlock();
}
}
}
synchronized仅一个等待队列,notify()随机唤醒;Condition支持多等待队列,实现 “精准唤醒”
✅ 使用建议(如何选择?)
| 场景 | 推荐使用 |
|---|---|
| 简单同步需求(方法 / 代码块级别) | ✅ synchronized(简洁、安全,无需手动管理锁) |
| 高并发、复杂控制(超时、中断、读写分离) | ✅ Lock(功能更强,性能更优) |
| 需要尝试获取锁或避免死锁 | ✅ Lock.tryLock() |
| 读多写少的共享资源(如缓存) | ✅ ReentrantReadWriteLock |
| 对性能要求极高且能保证锁安全释放 | ✅ Lock |
📝 面试回答模板
synchronized 是 JVM 层面的隐式锁,自动获取和释放,异常时安全释放锁,但不支持中断、超时和读写分离;Lock 是 JDK 层面的显式锁,需手动调用 lock()/unlock()(依赖 finally),支持三大核心优势:一是 中断等待(lockInterruptibly ()),二是 尝试获取锁(tryLock () 避免无限等待),三是 读写分离(ReadWriteLock 提升读并发),还可通过 Condition 实现多等待队列的精准唤醒;简单场景用 synchronized 更简洁,复杂高并发场景用 Lock 更灵活高效。
🔥 五、死锁与活锁的区别(重点)
✅ 核心区别
| 特征 | 死锁(Deadlock) | 活锁(Livelock) |
|---|---|---|
| 线程状态 | 被阻塞(Blocked),不再执行 | 运行状态(Runnable),持续尝试 |
| 资源占用 | 持有部分资源,等待其他资源不释放 | 主动释放资源并重试,但无进展 |
| 系统表现 | 相关线程完全停滞 | CPU 占用高,但无实际任务完成 |
| 是否在运行 | ❌ 实际已停止 | ✅ 仍在活跃运行 |
| 检测难度 | 相对容易(工具可检测循环等待) | 较难发现(看似正常运行) |
| 典型场景 | 多线程互相持有对方所需的锁 | 多线程为避免冲突持续让步 |
🔍 一、死锁(Deadlock)—— “互相等待,谁也不放手”
📌 定义
多个线程因竞争资源而相互等待,且每个线程都持有对方所需的资源,导致所有线程无法继续执行,陷入永久阻塞。
💡 四大必要条件(必须同时满足)
- 互斥条件:资源一次只能被一个线程使用(如锁的独占性)
- 占有并等待:线程已持有部分资源,仍在等待其他资源
- 不可抢占:已分配的资源不能被强制剥夺(如锁不能被其他线程强制释放)
- 循环等待:存在线程环形链,每个线程等待下一个线程持有的资源
🧪 经典示例:哲学家进餐问题
public class DeadlockExample {
// 两个叉子(资源)
private static final Object fork1 = new Object();
private static final Object fork2 = new Object();
public static void main(String[] args) {
// 线程A:先拿叉子1,再拿叉子2
new Thread(() -> {
synchronized (fork1) {
System.out.println("线程A持有叉子1,等待叉子2");
try {
Thread.sleep(100); // 模拟思考时间,让线程B有机会持有叉子2
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
synchronized (fork2) {
System.out.println("线程A持有叉子2,开始吃饭");
}
}
}).start();
// 线程B:先拿叉子2,再拿叉子1
new Thread(() -> {
synchronized (fork2) {
System.out.println("线程B持有叉子2,等待叉子1");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
synchronized (fork1) {
System.out.println("线程B持有叉子1,开始吃饭");
}
}
}).start();
}
}
结果:线程 A 持有 fork1 等待 fork2,线程 B 持有 fork2 等待 fork1,形成循环等待,陷入死锁。
🔁 二、活锁(Livelock)—— “积极退让,反而谁都做不了事”
📌 定义
线程未被阻塞,但因 “过度协作” 或 “避免冲突” 而不断重复相同动作(如主动释放资源、重试),导致任务始终无法完成,陷入 “忙碌但无效” 的循环。
🧪 经典生活比喻
两个人在狭窄走廊相遇,都礼貌地向同一侧避让 → 撞在一起;再换另一侧 → 又撞在一起,无限循环,即 “活锁”。
🧪 编程示例:银行转账中的活锁
public class LivelockExample {
static class Account {
private int balance;
public Account(int balance) {
this.balance = balance;
}
// 取款(成功返回true,失败返回false)
public boolean withdraw(int amount) {
if (amount <= balance) {
balance -= amount;
return true;
}
return false;
}
// 存款
public void deposit(int amount) {
balance += amount;
}
public int getBalance() {
return balance;
}
}
// 转账逻辑:余额不足时主动让出CPU,重试
public static void transfer(Account from, Account to, int amount) {
while (true) {
if (from.withdraw(amount)) {
to.deposit(amount);
System.out.println("转账成功,from余额:" + from.getBalance() + ",to余额:" + to.getBalance());
break;
} else {
System.out.println("余额不足,重试...");
Thread.yield(); // 主动让出CPU,避免占用资源
}
}
}
public static void main(String[] args) {
Account a = new Account(100);
Account b = new Account(100);
// 线程1:a向b转账150(余额不足,重试)
new Thread(() -> transfer(a, b, 150)).start();
// 线程2:b向a转账150(余额不足,重试)
new Thread(() -> transfer(b, a, 150)).start();
}
}
结果:两个线程均因余额不足持续重试并让出 CPU,陷入 “重试→失败→让出→再重试” 的循环,无法完成转账。
📝 面试回答模板
死锁和活锁的核心区别是 “线程状态” 和 “资源行为”:死锁的线程处于阻塞状态,持有部分资源并等待其他线程的资源,不释放自身资源,导致完全停滞;活锁的线程处于运行状态,主动释放资源并重试,但因 “同步重试” 或 “过度退让” 导致任务无进展,CPU 忙碌但无效。死锁需满足四大条件(互斥、占有等待、不可抢占、循环等待),活锁多因 “协作逻辑缺陷” 导致,检测难度更高。
🔥 六、如何避免活锁(重点)
✅ 四大常用避免活锁的策略
| 策略 | 说明 | 示例场景 |
|---|---|---|
| 1. 引入随机性 ✅ | 在重试时加入随机延迟,打破多个线程 “同步重试” 的节奏 | 分布式系统消息重发、网络请求重试 |
| 2. 指数退避(Exponential Backoff)✅ | 每次失败后等待时间成倍增长,减少竞争频率 | API 调用限流、数据库连接重连 |
| 3. 协调机制 / 优先级控制 | 明确角色分工或设定优先级,避免 “互相谦让” | 分布式锁选举、主从切换 |
| 4. 限制重试次数 ✅ | 设置最大重试次数,防止无限循环 | 所有需要重试的操作(如转账、请求) |
💡 经典代码示例:改进的重试机制(指数退避 + 随机化)
import java.util.Random;
public class AvoidLivelockUtil {
private static final int MAX_RETRIES = 5; // 最大重试次数
private static final long BASE_DELAY = 100; // 基础延迟(毫秒)
private static final Random random = new Random();
// 带指数退避和随机化的重试逻辑
public boolean sendDataWithBackoff() {
int attempts = 0;
while (attempts < MAX_RETRIES) {
if (sendData()) { // 尝试发送数据
System.out.println("第 " + (attempts + 1) + " 次尝试成功");
return true;
}
// 1. 指数退避:等待时间 = 2^attempts * 基础延迟
long delay = (long) Math.pow(2, attempts) * BASE_DELAY;
// 2. 加入随机扰动(±100ms),打破同步重试
delay += random.nextInt(200) - 100; // 随机范围:[-100, 100]
// 确保延迟不小于0
delay = Math.max(delay, 0);
try {
Thread.sleep(delay);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return false; // 中断则放弃重试
}
attempts++;
}
System.err.println("重试超过最大次数(" + MAX_RETRIES + "次),发送失败");
return false;
}
// 模拟网络发送(30%成功率)
private boolean sendData() {
return random.nextDouble() > 0.7;
}
}
📌 优点
- 指数退避:
Math.pow(2, attempts)让等待时间随重试次数倍增,减少频繁冲突 - 随机扰动:
random.nextInt(200) - 100避免多个线程 “同步重试” - 重试上限:
MAX_RETRIES防止无限循环,确保最终退出
🔍 为什么这些方法能避免活锁?
| 方法 | 破坏的活锁条件 |
|---|---|
| 随机延迟 | 打破线程间的 “同步节奏”,避免同时重试 |
| 指数退避 | 减少重试频率,降低资源竞争强度 |
| 限制重试次数 | 避免无限循环,保证最终能退出 |
| 优先级机制 | 明确 “谁先执行”,避免 “互相礼让” |
📝 面试回答模板
避免活锁的核心是 “打破无效循环”,常用四种策略:一是引入随机性,在重试间隔中加入随机时间,防止多线程同步重试;二是指数退避,每次失败后等待时间成倍增长(如 100ms→200ms→400ms),降低冲突频率;三是限制重试次数,设置最大重试上限,避免无限循环;四是建立协调机制,通过优先级或角色分配明确执行顺序,避免 “互相礼让”。实际场景中,常结合 “指数退避 + 随机化”(如分布式请求重试),既减少竞争又打破同步,效果最佳。
🔥 七、死锁的解决方案有哪些(重点)
🛠️ 一、尽量减少锁的使用(根本性预防)
核心思路
从源头减少锁的依赖,优先使用 “无锁结构” 或 “不可变对象” 替代同步操作。
适用场景与方案
| 场景 | 推荐方案 |
|---|---|
| 简单原子操作(如 i++、计数) | 使用 java.util.concurrent.atomic 包(AtomicInteger、AtomicLong) |
| 高并发读写场景 | 使用并发容器(ConcurrentHashMap、CopyOnWriteArrayList) |
| 无状态服务 | 设计不可变对象(如 String、Integer),避免共享状态 |
📌 示例:用 AtomicInteger 替代 synchronized
// ❌ 需加锁的计数
public class SynchronizedCounter {
private int count = 0;
public synchronized void increment() { count++; }
public synchronized int getCount() { return count; }
}
// ✅ 无锁计数(AtomicInteger 基于 CAS 实现)
public class AtomicCounter {
private AtomicInteger count = new AtomicInteger(0);
public void increment() { count.incrementAndGet(); }
public int getCount() { return count.get(); }
}
🔁 二、统一锁的获取顺序(破坏循环等待条件)
✅ 核心思想
让所有线程以相同的顺序申请多把锁,打破 “循环等待” 这一死锁必要条件(最实用的方案)。
🧪 示例:转账问题中的死锁改进
❌ 可能发生死锁的代码
// 线程1:先锁 fromA,再锁 toB;线程2:先锁 fromB,再锁 toA → 循环等待
public void transfer(Account from, Account to, int amount) {
synchronized (from) {
synchronized (to) {
from.balance -= amount;
to.balance += amount;
}
}
}
✅ 改进:按账户 ID 排序加锁
public class SafeTransfer {
static class Account {
private final int id; // 账户唯一ID
private int balance;
public Account(int id, int balance) {
this.id = id;
this.balance = balance;
}
public int getId() { return id; }
public int getBalance() { return balance; }
public void setBalance(int balance) { this.balance = balance; }
}
// 按账户ID升序获取锁,避免循环等待
public void safeTransfer(Account from, Account to, int amount) {
// 1. 确定锁的顺序(ID小的先锁)
Account firstLock = from.getId() < to.getId() ? from : to;
Account secondLock = from.getId() > to.getId() ? from : to;
// 2. 按顺序加锁
synchronized (firstLock) {
synchronized (secondLock) {
if (from.getBalance() >= amount) {
from.setBalance(from.getBalance() - amount);
to.setBalance(to.getBalance() + amount);
System.out.println("转账成功");
} else {
System.out.println("余额不足");
}
}
}
}
}
关键:所有线程均按 “ID 升序” 加锁,不会出现 “线程 1 锁 A 等 B,线程 2 锁 B 等 A” 的循环。
⏳ 三、使用带超时的锁(tryLock (timeout),避免无限等待)
核心思路
通过 ReentrantLock.tryLock(timeout) 设置锁的获取超时时间,超时后主动放弃,打破 “占有并等待” 条件。
🧪 示例:带超时的转账
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class TimeoutLockTransfer {
static class Account {
private final Lock lock = new ReentrantLock();
private int balance;
public Account(int balance) {
this.balance = balance;
}
public Lock getLock() { return lock; }
public int getBalance() { return balance; }
public void setBalance(int balance) { this.balance = balance; }
}
// 带超时的转账(超时时间3秒)
public boolean transferWithTimeout(Account from, Account to, int amount) throws InterruptedException {
// 1. 尝试获取“from”账户的锁(3秒超时)
if (from.getLock().tryLock(3, TimeUnit.SECONDS)) {
try {
// 2. 尝试获取“to”账户的锁(3秒超时)
if (to.getLock().tryLock(3, TimeUnit.SECONDS)) {
try {
if (from.getBalance() >= amount) {
from.setBalance(from.getBalance() - amount);
to.setBalance(to.getBalance() + amount);
System.out.println("转账成功");
return true;
} else {
System.out.println("余额不足");
return false;
}
} finally {
to.getLock().unlock(); // 释放to的锁
}
} else {
System.out.println("获取to账户锁超时");
return false;
}
} finally {
from.getLock().unlock(); // 释放from的锁
}
} else {
System.out.println("获取from账户锁超时");
return false;
}
}
}
✅ 优势
- 超时后主动退出,避免 “无限等待”
- 适用于响应时间敏感的系统(如金融交易、RPC 调用)
⚠️ 注意
- 需合理设置超时时间(过短易误判,过长仍可能阻塞)
- 超时后需有补偿机制(如记录日志、通知管理员、重试)
🔍 四、死锁检测 + 恢复机制
1. 工具检测(运维层面)
- jstack 命令:通过
jstack <进程ID>查看线程堆栈,JVM 会自动提示 “Found one Java-level deadlock” - 可视化工具:JConsole、VisualVM、Arthas 等,可实时监控线程状态,定位死锁线程
2. 程序级检测(高级用法)
通过 ThreadMXBean 实现死锁检测,发现死锁后主动中断线程:
import java.lang.management.ManagementFactory;
import java.lang.management.ThreadMXBean;
public class DeadlockDetector {
public static void main(String[] args) {
// 启动死锁检测线程(每5秒检测一次)
new Thread(() -> {
ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
while (true) {
// 检测死锁线程ID
long[] deadlockedThreadIds = threadMXBean.findDeadlockedThreads();
if (deadlockedThreadIds != null && deadlockedThreadIds.length > 0) {
System.err.println("检测到死锁,线程ID:" + Arrays.toString(deadlockedThreadIds));
// 恢复策略:中断其中一个线程(如第一个)
Thread deadThread = findThreadById(deadlockedThreadIds[0]);
if (deadThread != null) {
deadThread.interrupt();
System.err.println("已中断线程:" + deadThread.getId());
}
}
try {
Thread.sleep(5000); // 每5秒检测一次
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
}
}).start();
}
// 根据线程ID查找线程(需遍历线程组)
private static Thread findThreadById(long threadId) {
ThreadGroup group = Thread.currentThread().getThreadGroup();
while (group != null) {
Thread[] threads = new Thread[group.activeCount()];
int count = group.enumerate(threads);
for (int i = 0; i < count; i++) {
if (threads[i].getId() == threadId) {
return threads[i];
}
}
group = group.getParent();
}
return null;
}
}
📌 应用场景
- 数据库系统:检测死锁后,选择一个 “代价最小” 的事务回滚(如执行时间最短的)
- 分布式系统:通过 “心跳检测” 或 “协调器” 发现死锁,触发服务熔断或重启
🧩 五、破坏死锁四大必要条件(理论基础)
| 死锁条件 | 破坏方式 |
|---|---|
| 互斥条件 | 很难破坏(锁的本质是互斥,适合资源不可共享场景) |
| 占有并等待 | 一次性申请所有所需资源(如银行家算法),避免 “持有部分资源等待” |
| 不可抢占 | 允许资源被抢占(如通过 tryLock(timeout) 超时释放,或线程中断释放锁) |
| 循环等待 | 统一锁的获取顺序(最实用,如按 ID 排序) |
📝 面试回答模板
死锁的解决方案围绕 “破坏死锁四大必要条件” 展开,核心有五种方案:一是减少锁的使用,优先用 Atomic 类、并发容器等无锁结构,从源头降低风险;二是统一锁顺序,让所有线程按相同顺序(如 ID 升序)获取多把锁,打破循环等待;三是使用带超时的锁,通过 tryLock(timeout) 让线程超时后主动放弃,避免无限等待;四是死锁检测与恢复,用 jstack 等工具排查,或程序中通过 ThreadMXBean 检测,发现后中断线程;五是破坏占有并等待,一次性申请所有资源(如银行家算法)。实际开发中,“统一锁顺序” 和 “减少锁使用” 最常用,简单且有效。
💡 附:Java 并发编程面试高频问题速查表
| 问题类型 | 高频问题 | 核心要点 |
|---|---|---|
| 基础概念 | 线程与进程的区别 | 线程是进程的子集,共享进程资源;进程间内存隔离,切换开销大 |
| 什么是线程安全 | 多线程环境下,代码执行结果与单线程一致,无数据竞争或不一致 | |
| volatile 关键字的作用 | 保证可见性、禁止指令重排序;不保证原子性,不能替代锁 | |
| 锁机制 | synchronized 和 Lock 的区别 | 见第四部分(隐式 vs 显式、中断支持、读写分离等) |
| 什么是可重入锁 | 同一线程可多次获取同一把锁(如 synchronized、ReentrantLock),避免自死锁 | |
| 公平锁与非公平锁 | 公平锁按申请顺序获取,性能低;非公平锁允许插队,性能高(默认) | |
| 线程通信 | wait ()、notify ()、notifyAll () 的区别 | 见第一部分(唤醒数量、安全性、使用场景) |
| sleep () 和 wait () 的区别 | sleep 不释放锁,是 Thread 方法;wait 释放锁,是 Object 方法,需在同步块中调用 | |
| 并发工具 | ConcurrentHashMap 实现原理 | JDK8 前:分段锁(Segment);JDK8 后:CAS + synchronized(Node 锁),粒度更细 |
| CountDownLatch 和 CyclicBarrier 的区别 | 前者计数递减(不可复用),后者计数递增(可复用);前者等待线程不阻塞计数线程,后者所有线程阻塞 | |
| 问题排查 | 如何排查死锁 | jstack 命令、JConsole/VisualVM 工具、ThreadMXBean 程序检测 |
| 如何避免死锁 | 统一锁顺序、减少锁范围、超时锁、无锁结构 |
📌 总结:Java 并发编程核心思维
- 先思考,再加锁:不盲目使用同步,先判断是否真的需要共享状态,优先用无锁方案。
- 锁粒度越小越好:仅锁定临界区代码,将耗时操作(I/O、日志)移出同步块。
- 避免嵌套锁:若需多把锁,确保所有线程按相同顺序获取,防止循环等待。
- 善用并发工具:优先使用 JUC 包(Atomic、Concurrent*、Lock),而非原始 synchronized。
- 理解内存模型:掌握 happens-before 原则,解决可见性、有序性、原子性问题。
- 设计优于补救:系统设计阶段考虑并发场景,而非事后修复死锁、活锁等问题。
更多推荐



所有评论(0)