大白话拆解——多线程中关于死锁的一切(七)(已完结)

前言:

25年初,这个时候好多小伙伴都在备战期末

小编明天还有一科考试,日更一篇,今天这篇一定会对小白非常有用的!!!

因为我们会把案例到用代码实现的全过程思路呈现出来!!!

我们一直都是以这样的形式,让新手小白轻松理解复杂晦涩的概念,把Java代码拆解的清清楚楚,每一步都知道他是怎么来的,为什么用这串代码关键字,对比同类型的代码,让大家真正看完以后融会贯通,举一反三,实践应用!!!!



①官方定义  和  大白话拆解对比

②举生活中常见贴合例子、图解辅助理解的形式

③对代码实例中关键部分进行详细拆解、总结


我们今天就不回顾上篇的内容了,直接继续

6.2 死锁

官方语言:

  • 死锁(Deadlock)是指在多线程或多进程环境中,两个或多个线程或进程因互相持有对方需要的资源而不放弃,并且都在等待对方释放资源的一种阻塞现象。
  • 在这种情况下,没有一个线程或进程能够继续执行,因为它们都卡在等待状态,导致整个系统或程序的部分功能停滞不前。
  • 死锁是并发编程中常见的问题之一,通常与同步机制如锁(Lock)、互斥量(Mutex)、信号量(Semaphore)等有关。

死锁发生的必要条件包括:

  • 互斥条件:至少有一个资源必须以非共享模式存在,即一次只能被一个线程占有。
  • 占有并等待条件:一个线程必须占用至少一个资源,并等待获取当前被其他线程占用的额外资源。
  • 不可剥夺条件:资源不能被强制从占用它的线程那里夺走;资源只能由占用它的线程主动释放。
  • 循环等待条件:存在一个线程的循环链,每个线程都拥有下一个线程所需要的资源。


大白话拆解:

  • 想象一下你和你的朋友正在玩交换礼物的游戏,但你们俩同时拿起了对方想要的礼物,然后都不愿意先放下自己的礼物去拿对方的。这样你们就陷入了僵局,谁也无法完成交换礼物的动作,因为大家都在等着对方先行动。这种情况在计算机科学里被称为“死锁”。
  • 你和你的室友都有各自的电脑,但是现在你需要用他的电脑上的某个软件,他需要你电脑上的另一个软件。你们俩决定互换电脑使用,但当你们走到彼此面前的时候,发现对方正坐在自己的电脑前,等着你先让出位置。于是你们俩就在那儿坐着,互相等待对方先起身,结果就是谁也没能用上对方的电脑,形成了“死锁”。

举个栗子:

银行转账系统的场景,其中有两个账户Account A和Account B,以及两个线程Thread 1和Thread 2。

  • 这两个线程负责处理不同用户发起的转账请求。假设Thread 1要将钱从Account A转到Account B,而Thread 2要将钱从Account B转到Account A。如果两个线程几乎同时开始操作,可能会发生以下情况:
  • Thread 1获得了Account A的锁,准备从中取款。
  • Thread 2获得了Account B的锁,准备从中取款。
  • 接下来,Thread 1尝试获得Account B的锁以便向其存款,但是因为Thread 2已经持有了这个锁,所以Thread 1进入等待状态。
  • 同时,Thread 2也尝试获得Account A的锁以便向其存款,但是因为Thread 1已经持有了这个锁,所以Thread 2也进入等待状态。
  • 此时,Thread 1在等待Thread 2释放Account B的锁,而Thread 2在等待Thread 1释放Account A的锁,两者都不会主动放弃自己持有的锁,从而形成死锁。
public class DeadlockExample {// 定义两个账户static Account accountA = new Account("Account A");static Account accountB = new Account("Account B");public static void main(String[] args) throws InterruptedException {// 创建两个线程,每个线程负责一个转账操作Thread thread1 = new Thread(new TransferTask(accountA, accountB, 100), "Thread 1");Thread thread2 = new Thread(new TransferTask(accountB, accountA, 50), "Thread 2");// 启动线程thread1.start();thread2.start();// 等待线程结束thread1.join();thread2.join();}
}class Account {private String name;private int balance;public Account(String name) {this.name = name;this.balance = 1000; // 初始余额}public synchronized void withdraw(int amount) {if (amount > balance) {System.out.println(Thread.currentThread().getName() + ": Insufficient funds in " + name);return;}balance -= amount;System.out.println(Thread.currentThread().getName() + ": Withdrew " + amount + " from " + name);}public synchronized void deposit(int amount) {balance += amount;System.out.println(Thread.currentThread().getName() + ": Deposited " + amount + " into " + name);}@Overridepublic String toString() {return "Account{" +"name='" + name + '\'' +", balance=" + balance +'}';}
}class TransferTask implements Runnable {private final Account fromAccount;private final Account toAccount;private final int amount;public TransferTask(Account fromAccount, Account toAccount, int amount) {this.fromAccount = fromAccount;this.toAccount = toAccount;this.amount = amount;}@Overridepublic void run() {// 尝试获取两个账户的锁synchronized (fromAccount) {System.out.println(Thread.currentThread().getName() + ": Locked " + fromAccount.getName());fromAccount.withdraw(amount); // 从来源账户取款try {// 模拟处理延迟Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}synchronized (toAccount) { // 尝试锁定目标账户System.out.println(Thread.currentThread().getName() + ": Locked " + toAccount.getName());toAccount.deposit(amount); // 向目标账户存款}}}
}

代码解释和总结

  • Account 类:这个类代表一个银行账户。它有两个方法,withdraw 用来取钱,deposit 用来存钱。这两个方法前都有 synchronized 关键字,这意味着如果一个线程正在使用 withdraw 或 deposit 方法,那么其他线程必须等待,直到该线程完成操作并释放了账户的锁。
  • TransferTask 类:这个类代表一个转账任务。它实现了 Runnable 接口,所以它可以被一个线程执行。它的 run 方法定义了当线程开始运行时要做的工作。在这个例子中,它会先锁定来源账户,然后尝试锁定目标账户。如果另一个线程已经锁定了目标账户,当前线程就会等待,直到目标账户的锁被释放。
  • DeadlockExample 类:这是主类,包含 main 方法,是程序的入口点。这里我们创建了两个账户和两个线程。每个线程都试图执行一个转账操作,但是由于它们几乎同时启动,并且按照不同的顺序尝试锁定两个账户,就有可能形成死锁。
  • 同步块:在 TransferTask 的 run 方法中,我们使用了 synchronized 块来确保在同一时间只有一个线程可以访问特定的账户。这就像是一把锁,一次只能有一个人进入房间。
  • 模拟处理延迟:Thread.sleep(100); 这一行是用来模拟实际转账过程中可能会有的处理时间。在这段时间里,线程保持持有账户的锁,增加了死锁的可能性。

死锁发生的原因

  • 在这个例子中,死锁可能发生是因为两个线程以相反的顺序尝试获取相同的资源(即两个账户的锁)。具体来说:
  • Thread 1 先锁住了 Account A,然后尝试去锁住 Account B。
  • Thread 2 先锁住了 Account B,然后尝试去锁住 Account A。
  • 如果两个线程恰好在对方尝试获取自己持有的锁之前成功获取了第一个锁,那么它们都会卡在那里,等待对方释放锁,而对方也在等自己释放锁,结果就是谁都无法继续前进,形成了死锁。

诱发死锁的原因:

我们还是以上面的代码为例,

上面例子中:TransferTask类实现了转账操作,它在执行时会尝试获取两个账户对象的锁。由于两个线程分别试图以相反的顺序锁定相同的两个账户(accountA和accountB),这就创建了一个潜在的死锁场景。具体来说,死锁可能发生的原因如下:

  • 互斥条件:每个账户上的synchronized方法确保在同一时间只有一个线程可以访问该账户。这是产生死锁的第一个必要条件。
  • 持有并等待资源:当一个线程已经持有一个锁(比如fromAccount),然后去尝试获取另一个锁(toAccount)时,如果此时另一个线程也正在持有第二个锁而尝试获取第一个锁,就会出现这种情况。这满足了死锁的第二个条件。
  • 非抢占条件:Java的锁是不可抢占的,即一旦一个线程获得了锁,它就不能被强制释放,只有当线程自己释放锁的时候才能释放。这满足了死锁的第三个条件。
  • 循环等待条件:这里存在一个潜在的循环等待链,例如:
  • Thread 1 持有 accountA 的锁,并等待 accountB 的锁。
  • Thread 2 持有 accountB 的锁,并等待 accountA 的锁。 这样就形成了一个循环等待链,满足了死锁的第四个条件。

解决死锁:

策略1:一次性申请所有所需的资源


大白话拆解:

  • 你和你的朋友打算一起做饭,你们需要不同的厨具。为了避免两个人同时拿起同一个厨具而卡住,你们可以事先商量好,每个人一次性拿走自己需要的所有厨具。这样,当一个人开始做饭时,他已经有了所有需要的东西,不需要再等其他人腾出厨具了。

应用到代码:

  • 在转账操作开始之前,我们可以尝试一次性获取两个账户的锁,而不是先获取一个账户的锁,然后再获取另一个。这可以通过尝试以相同的顺序锁定两个账户来实现,确保不会发生交叉锁定的情况。
class TransferTask implements Runnable {private final Account fromAccount;private final Account toAccount;private final int amount;public TransferTask(Account fromAccount, Account toAccount, int amount) {this.fromAccount = fromAccount;this.toAccount = toAccount;this.amount = amount;}@Overridepublic void run() {// 以固定的顺序锁定账户,避免交叉锁定Object firstLock, secondLock;if (fromAccount.hashCode() < toAccount.hashCode()) {firstLock = fromAccount;secondLock = toAccount;} else {firstLock = toAccount;secondLock = fromAccount;}synchronized (firstLock) {System.out.println(Thread.currentThread().getName() + ": Locked " + ((Account) firstLock).getName());synchronized (secondLock) {System.out.println(Thread.currentThread().getName() + ": Locked " + ((Account) secondLock).getName());fromAccount.withdraw(amount);toAccount.deposit(amount);}}}
}

策略2:占用部分资源的线程在进一步申请其他资源时,如果申请不到,就主动释放掉已经占用的资源


大白话拆解:

  • 还是做饭的例子,如果你发现自己没有拿到全部需要的厨具,你可以选择把已经拿到的厨具放回去,让别人先使用,等他们用完后再重新尝试获取所有你需要的厨具。

应用到代码:

  • 我们可以尝试获取第二个锁,如果获取失败,那么我们就释放第一个锁,然后可能重试或者退出操作。Java 中的 tryLock 方法可以帮助我们实现这一点。
import java.util.concurrent.locks.ReentrantLock;class TransferTask implements Runnable {private final ReentrantLock lockA = new ReentrantLock();private final ReentrantLock lockB = new ReentrantLock();private final Account fromAccount;private final Account toAccount;private final int amount;public TransferTask(Account fromAccount, Account toAccount, int amount) {this.fromAccount = fromAccount;this.toAccount = toAccount;this.amount = amount;}@Overridepublic void run() {boolean hasLockA = false;boolean hasLockB = false;try {hasLockA = lockA.tryLock(500, TimeUnit.MILLISECONDS);if (!hasLockA) {System.out.println(Thread.currentThread().getName() + ": Could not acquire lock on " + fromAccount.getName());return;}hasLockB = lockB.tryLock(500, TimeUnit.MILLISECONDS);if (!hasLockB) {System.out.println(Thread.currentThread().getName() + ": Could not acquire lock on " + toAccount.getName());return;}// 成功获取两个锁后进行转账fromAccount.withdraw(amount);toAccount.deposit(amount);} catch (InterruptedException e) {e.printStackTrace();} finally {if (hasLockA) lockA.unlock();if (hasLockB) lockB.unlock();}}
}

策略3:将资源改为线性顺序


大白话拆解:

  • 如果我们有一个规则,比如说所有人都按照从左到右的顺序去拿厨具,就不会出现两个人互相等待对方放开手的情况了。

应用到代码:

  • 我们可以根据账户对象的哈希码(或任何其他唯一标识符)来决定哪个账户应该首先被锁定。这样可以保证所有线程总是以相同的顺序锁定账户,从而避免循环等待。
  • 上面的策略1已经展示了这种做法,我们就不用具体代码了再


我们今天就先到这里

下篇再见吧!!!

看在小编日更的份儿上了,点个关注好不好,我们一起进步!!!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/891776.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

GitLab集成Runner详细版--及注意事项汇总【最佳实践】

一、背景 看到网上很多用户提出的runner问题其实实际都不是问题&#xff0c;不过是因为对runner的一些细节不清楚导致了误解。本文不系统性的介绍GitLab-Runner&#xff0c;因为这类文章写得好的特别多&#xff0c;本文只汇总一些常几的问题/注意事项。旨在让新手少弯路。 二、…

《数据结构》期末考试测试题【中】

《数据结构》期末考试测试题【中】 21.循环队列队空的判断条件为&#xff1f;22. 单链表的存储密度比1&#xff1f;23.单链表的那些操作的效率受链表长度的影响&#xff1f;24.顺序表中某元素的地址为&#xff1f;25.m叉树第K层的结点数为&#xff1f;26. 在双向循环链表某节点…

「Mac畅玩鸿蒙与硬件54」UI互动应用篇31 - 滑动解锁屏幕功能

本篇教程将实现滑动解锁屏幕功能&#xff0c;通过 Slider 组件实现滑动操作&#xff0c;学习事件监听、状态更新和交互逻辑的实现方法。 关键词 滑动解锁UI交互状态管理动态更新事件监听 一、功能说明 滑动解锁屏幕功能包含以下功能&#xff1a; 滑动解锁区域&#xff1a;用…

螺栓松动丢失腐蚀生锈检测数据集VOC+YOLO格式504张4类别

数据集格式&#xff1a;Pascal VOC格式YOLO格式(不包含分割路径的txt文件&#xff0c;仅仅包含jpg图片以及对应的VOC格式xml文件和yolo格式txt文件) 图片数量(jpg文件个数)&#xff1a;504 标注数量(xml文件个数)&#xff1a;504 标注数量(txt文件个数)&#xff1a;504 标注…

Postman测试big-event

报错500。看弹幕&#xff0c;知道可能是yml或sql有问题。 所以检查idea工作台&#xff0c; 直接找UserMapper检查&#xff0c;发现完全OK。 顺着这个error发现可能是sql有问题。因为提示是sql问题&#xff0c;而且是有now()的那个sql。 之后通过给的课件&#xff0c;复制课件…

如何使用大语言模型进行事件抽取与关系抽取

诸神缄默不语-个人CSDN博文目录 文章目录 1. 什么是事件抽取与关系抽取&#xff1f;2. 示例&#xff1a;使用大语言模型进行事件抽取与关系抽取 1. 什么是事件抽取与关系抽取&#xff1f; 事件抽取是指从文本中识别出与某些“事件”相关的信息。这些事件通常包括动作、参与者、…

NAT网络技术

NAT&#xff08;Network Address Translation&#xff0c;网络地址转换&#xff09;是一种常用的网络技术&#xff0c;主要用于在私有网络和公共网络之间转换IP地址。在家庭和小型企业网络当中用的比较多。它的主要功能有IP地址重用和增强网络的安全性。   NAT允许一个整个网…

SpringBoot框架开发中常用的注解

文章目录 接收HTTP请求。RestController全局异常处理器Component依赖注入LombokDataBuildersneakyThrowsRequiredArgsConstructor 读取yml文件配置类注解 接收HTTP请求。 RequestMapping 接收HTTP请求。具体一点是 GetMapping PostMapping PutMapping DeleteMapping 一共…

TVS二极管选型【EMC】

TVS器件并联在电路中&#xff0c;当电路正常工作时&#xff0c;他处于截止状态&#xff08;高阻态&#xff09;&#xff0c;不影响线路正常工作&#xff0c;当线路处于异常过压并达到其击穿电压时&#xff0c;他迅速由高阻态变为低阻态&#xff0c;给瞬间电流提供一个低阻抗导通…

Azkaban其二,具体使用以及告警设置

目录 Azkaban的使用 1、使用Flow1.0(比较老旧&#xff09; 2、Flow2.0的用法 1、小试牛刀 2、YAML格式的数据 3、多任务依赖 4、内嵌流&#xff08;嵌套流&#xff09;案例 5、动态传参 3、Azkaban的报警机制 1&#xff09;邮箱通知 2&#xff09;电话报警机制 4、关…

文档 | Rstudio下的轻量级单页面markdown阅读器 markdownReader

需求&#xff1a;在写R数据分析项目的时候&#xff0c;代码及结果的关键变化怎么记录下来&#xff1f;最好git能很容易的跟踪版本变化。 markdown 是最理想的选择&#xff0c;本文给出一种Rstuidio下的轻量级md阅读器实现&#xff1a;markdownReader。书写md还是在Rstudio。更…

SonarQube相关的maven配置及使用

一、maven 全局配置 <settings><pluginGroups><pluginGroup>org.sonarsource.scanner.maven</pluginGroup></pluginGroups><profiles><profile><id>sonar</id><activation><activeByDefault>true</acti…

Arduino Uno简介与使用方法

目录 一、Arduino Uno概述 1. 硬件特性 2. 开发环境 二、Arduino Uno的基本使用方法 1. 硬件连接 2. 软件编程 三、Arduino Uno编程基础 1. 基本语法 2. 常用函数 四、Arduino Uno应用举例 1. LED闪烁 2. 温度检测 3. 超声波测距 五、Arduino Uno的扩展与应用 1…

UniApp | 从入门到精通:开启全平台开发的大门

UniApp | 从入门到精通:开启全平台开发的大门 一、前言二、Uniapp 基础入门2.1 什么是 Uniapp2.2 开发环境搭建三、Uniapp 核心语法与组件3.1 模板语法3.2 组件使用四、页面路由与导航4.1 路由配置4.2 导航方法五、数据请求与处理5.1 发起请求5.2 数据缓存六、样式与布局6.1 样…

滑动窗口。

1456 定长子串中元音的最大数目 采用滑动窗口。每次移动一个位置&#xff0c;判断当前窗口内的子串内目标元素的个数&#xff0c;若比之前更大就更新结果。 如何判断是否更新结果&#xff1f;也即&#xff0c;如何判断当前窗口内所含目标元素个数&#xff0c;是否为遍历到这个…

公共数据授权运营系统建设手册(附下载)

在全球范围内&#xff0c;许多国家和地区已经开始探索公共数据授权运营的路径和模式。通过建立公共数据平台&#xff0c;推动数据的开放共享&#xff0c;促进数据的创新应用&#xff0c;不仅能够提高政府决策的科学性和公共服务的效率&#xff0c;还能够激发市场活力&#xff0…

电脑主机后置音频插孔无声?还得Realtek高清晰音频管理器调教

0 缘起 一台联想电脑&#xff0c;使用Windows 10 专业版32位&#xff0c;电脑主机后置音频插孔一直没有声音&#xff0c;所以音箱是接在机箱前面版的前置音频插孔上的。 一天不小心捱到了音箱的音频线&#xff0c;音频线头断在音频插孔里面了&#xff0c;前置音频插孔因此用不…

【微服务】1、引入;注册中心;OpenFeign

微服务技术学习引入 - 微服务自2016年起搜索指数持续增长&#xff0c;已成为企业开发大型项目的必备技术&#xff0c;中高级java工程师招聘多要求熟悉微服务相关技术。微服务架构介绍 概念&#xff1a;微服务是一种软件架构风格&#xff0c;以专注于单一职责的多个响应项目为基…

UDP_TCP

目录 1. 回顾端口号2. UDP协议2.1 理解报头2.2 UDP的特点2.3 UDP的缓冲区及注意事项 3. TCP协议3.1 报头3.2 流量控制2.3 数据发送模式3.4 捎带应答3.5 URG && 紧急指针3.6 PSH3.7 RES 1. 回顾端口号 在 TCP/IP 协议中&#xff0c;用 “源IP”&#xff0c; “源端口号”…

《Spring Framework实战》2:Spring快速入门

欢迎观看《Spring Framework实战》视频教程 Spring快速入门 目录 1. Java™开发套件&#xff08;JDK&#xff09; 2. 集成开发人员环境&#xff08;IDE&#xff09; 3. 安装Maven 4. Spring快速入门 4.1. 开始一个新的Spring Boot项目 4.2. 添加您的代码 4.3. 尝…