当前位置: 首页 > news >正文

【多线程篇21】:深入浅出理解Java死锁

文章目录

  • 1. 什么是死锁?
  • 2. 死锁的“案发现场”:代码复现
  • 3. 死锁产生的四个“必要条件”
  • 4. 如何“破解”死锁?
    • 方案一:破坏“循环等待”条件(最常用)
    • 方案二:破坏“请求与保持”条件
    • 方案三:破坏“不可剥夺”条件
  • 总结

1. 什么是死锁?

用一句话来概括:死锁是指两个或多个线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力干涉,它们将都无法再向下推进。

听起来有点抽象,我们来看一个生活中的例子:

想象一下一座只能容纳一个人通过的独木桥。

线程 A 从桥的左边走到了中间,此时它持有了“桥的左半边”,并等待“桥的右半边”空出来。
线程 B 同时从桥的右边走到了中间,此时它持有了“桥的右半边”,并等待“桥的左半边”空出来。

结果呢?A 等着 B 后退,B 等着 A 后退,谁也不肯让步,两个人就永远僵持在了桥中间。

在 Java 中,这个“独木桥”就是锁(Lock)。当一个线程需要同时获取多把锁时,就极易发生死锁。

  • t1 线程:获得了 A 对象的锁,接下来想获取 B 对象的锁。
  • t2 线程:获得了 B 对象的锁,接下来想获取 A 对象的锁。

此时,t1 因等待 B 锁而阻塞,t2 因等待 A 锁而阻塞,两个线程都在互相等待对方释放自己需要的锁,最终导致了死锁。


2. 死锁的“案发现场”:代码复现

我们用一段经典的代码来复现死锁的“案发现场”。

public class Deadlock {public static void main(String[] args) {// 创建两个锁对象Object lockA = new Object();Object lockB = new Object();// 线程 t1Thread t1 = new Thread(() -> {synchronized (lockA) {System.out.println("t1: 已持有 lockA,准备获取 lockB...");try {// 确保 t2 有机会获取 lockBsleep(200);} catch (InterruptedException e) {throw new RuntimeException(e);}synchronized (lockB) {System.out.println("t1: 已持有 lockB");System.out.println("t1: 执行操作...");}}}, "t1");// 线程 t2Thread t2 = new Thread(() -> {synchronized (lockB) {System.out.println("t2: 已持有 lockB,准备获取 lockA...");try {} catch (InterruptedException e) {throw new RuntimeException(e);}synchronized (lockA) {System.out.println("t2: 已持有 lockA");System.out.println("t2: 执行操作...");}}}, "t2");t1.start();try {// 确保 t2 有机会获取 lockBsleep(100);} catch (InterruptedException e) {throw new RuntimeException(e);}t2.start();}
}

执行流程分析:

  1. t1 启动,立刻获得 lockA 的锁,打印 “t1: 已持有 lockA…”,然后 sleep(1000)
  2. t1 睡眠期间,t2 启动,立刻获得 lockB 的锁,打印 “t2: 已持有 lockB…”,然后 sleep(500)
  3. 500 毫秒后,t2 醒来,尝试获取 lockA。但此时 lockA 仍被 t1 持有,因此 t2 进入阻塞状态,等待 lockA
  4. 1000 毫秒后,t1 醒来,尝试获取 lockB。但此时 lockB 仍被 t2 持有,因此 t1 进入阻塞状态,等待 lockB

控制台输出:

t1: 已持有 lockA,准备获取 lockB...
t2: 已持有 lockB,准备获取 lockA...

此时,程序光标会一直闪烁,但不会有任何新的输出,也不会结束。这就是典型的死锁现象!t1 等着 t2 释放 lockB,而 t2 等着 t1 释放 lockA,形成了一个完美的“死亡闭环”。


3. 死锁产生的四个“必要条件”

一个问题之所以发生,必然有其底层的原因。死锁的发生,需要同时满足以下四个缺一不可的条件:

  1. 互斥条件(Mutual Exclusion)

    • 定义:一个资源在同一时刻只能被一个线程占用。
    • 解释:锁的基本特性。如果资源可以被共享,也就不会有争抢和等待了。
  2. 请求与保持条件(Hold and Wait)

    • 定义:一个线程因请求资源而阻塞时,对已获得的资源保持不放。
    • 解释:就像上面例子中的 t1,它已经持有了 lockA,在请求 lockB 失败后,它并不会释放 lockA
  3. 不可剥夺条件(No Preemption)

    • 定义:线程已获得的资源,在未使用完之前,不能被其他线程强行剥夺,只能在使用完后由自己释放。
    • 解释synchronized 锁就是不可剥夺的。操作系统不会因为 t2 需要 lockA,就强行从 t1 手里把 lockA 抢过来。
  4. 循环等待条件(Circular Wait)

    • 定义:发生死锁时,必然存在一个线程—资源的循环等待链。
    • 解释t1 等待 t2 的资源,t2 等待 t1 的资源,形成 t1 -> t2 -> t1 的等待环路。

核心要点:这四个条件必须同时满足才会发生死锁。因此,只要我们能破坏其中任意一个条件,就能有效预防死锁。


4. 如何“破解”死锁?

了解了死锁的成因,我们就可以对症下药,通过破坏上述四个条件来避免死锁。

方案一:破坏“循环等待”条件(最常用)

这是最常用也是最有效的避免死锁的方法。其核心思想是:规定所有线程必须按照相同的顺序来获取锁

在我们的例子中,t1 的加锁顺序是 A -> B,而 t2 的顺序是 B -> A,这正是导致循环等待的元凶。我们只需将 t2 的加锁顺序也改为 A -> B 即可。

修改 t2 的代码:

// 线程 t2 (修改后)
Thread t2 = new Thread(() -> {// 同样先尝试获取 lockAsynchronized (lockA) {System.out.println("t2: 已持有 lockA,准备获取 lockB...");try {sleep(500);} catch (InterruptedException e) {throw new RuntimeException(e);}// 然后再获取 lockBsynchronized (lockB) {System.out.println("t2: 已持有 lockB");System.out.println("t2: 执行操作...");}}
}, "t2");

这样修改后,无论是 t1 还是 t2,谁先抢到 lockA,谁就能继续获取 lockB 并顺利执行完毕,然后释放所有锁。另一个线程则会等待 lockA,并在前者执行完后获得锁,继续执行。这样就将**“并发”变为了“串行”**,彻底避免了死锁。

方案二:破坏“请求与保持”条件

这个方案要求线程一次性申请所有需要的资源(锁),如果无法全部获得,则一个也不占有。

这种方式在 synchronized 语法层面不易直接实现,因为它无法一次性获取多个锁。但在实际业务中,可以把多个需要加锁的资源封装成一个对象,只对这个大对象加锁,从而间接实现。不过,这会降低程序的并发度。

方案三:破坏“不可剥夺”条件

synchronized 是不可剥夺的,但 java.util.concurrent.locks 包下的 Lock 接口提供了更灵活的加锁方式。例如,lock.tryLock() 方法。

tryLock(long time, TimeUnit unit) 方法会尝试在指定时间内获取锁:

  • 如果成功获取,返回 true
  • 如果在超时前仍未获取到锁,返回 false,而不是一直阻塞。

利用这个特性,线程在获取锁失败后,可以选择释放自己已经持有的锁,然后重试,从而打破了“不可剥夺”的僵局。


总结

死锁是多线程编程中一个经典且重要的话题。通过本文,我们应该掌握以下核心知识:

  1. 死锁定义:多个线程因互相等待对方持有的资源而陷入的无限阻塞状态。
  2. 死锁复现:最经典的场景就是两个线程以相反的顺序获取两把锁。
  3. 死锁四要素:互斥、请求与保持、不可剥夺、循环等待,四者缺一不可。
  4. 死锁预防:最关键、最简单的策略就是保证所有线程以相同的顺序获取锁,从而破坏“循环等待”条件。
http://www.dtcms.com/a/292816.html

相关文章:

  • 《Uniapp-Vue 3-TS 实战开发》自定义预约时间段组件
  • 7.22总结mstp,vrrp
  • WebSocket心跳机制实现要点
  • 京东AI投资版图扩张:具身智能与GPU服务器重构科研新范式
  • 小鹏汽车视觉算法面试30问全景精解
  • 学习游戏制作记录(战斗系统简述以及击中效果)7.22
  • 为什么使用扩展坞会降低显示器的最大分辨率和刷新率
  • 智能泵房监控系统:物联网应用与智能管理解决方案
  • 【观察】维谛技术(Vertiv)“全链智算”:重构智算中心基础设施未来演进范式
  • 如何编译RustDesk(Unbuntu 和Android版本)
  • Cookies 详解及其与 Session 的协同工作
  • AWS OpenSearch 搜索排序常见用法
  • 2️⃣tuple(元组)速查表
  • C语言面向对象编程
  • Java函数式编程深度解析:从基础到高阶应用
  • Leetcode题解:209长度最小的子数组,掌握滑动窗口从此开始!!!
  • 光伏电站智能数据采集系统解决方案
  • SpringBoot PO VO BO POJO实战指南
  • 十进制小数转换为二进制表示 ← 除2取余法+乘2取整法
  • csp基础知识——递推
  • SMTP+VRRP实验
  • Markdown 转 PDF API 数据接口
  • REASONING ELICITATION IN LANGUAGE MODELSVIA COUNTERFACTUAL FEEDBACK
  • 高性能线程安全的时间有序 UUID 生成器 —— 基于 ThreadLocal 的实现
  • 实操:AWS CloudFront的动态图像转换
  • Cadence 原理图如何给网络名称添加页码
  • 『React』条件渲染的7种方法
  • 基于Prompt 的DevOps 与终端重塑
  • 装备数字孪生底座平台探索
  • HTTP 协议常见字段(请求头/响应头)