欢迎来到尧图网

客户服务 关于我们

您的位置:首页 > 新闻 > 焦点 > 大白话拆解——多线程中关于死锁的一切(七)(已完结)

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

2025/1/7 4:20:32 来源:https://blog.csdn.net/2401_86356924/article/details/144934305  浏览:    关键词:大白话拆解——多线程中关于死锁的一切(七)(已完结)

前言:

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已经展示了这种做法,我们就不用具体代码了再


我们今天就先到这里

下篇再见吧!!!

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

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com