【多线程】死锁
【多线程】死锁
本文来自于我关于多线程系列文章。欢迎阅读、点评与交流
1.【多线程】互斥锁(Mutex)是什么?
2.【多线程】临界区(Critical Section)是什么?
3.【多线程】计算机领域中的各种锁
4.【多线程】信号量(Semaphore)是什么?
5.【多线程】信号量(Semaphore)常见的应用场景
6.【多线程】条件变量(Condition Variable)是什么?
7.【多线程】监视器(Monitor)是什么?
8.【多线程】什么是原子操作(Atomic Operation)?
9.【多线程】竞态条件(race condition)是什么?
10.【多线程】无锁数据结构(Lock-Free Data Structures)是什么?
11.【多线程】线程休眠(Thread Sleep)的底层实现
12.【多线程】多线程的底层实现
13.【多线程】读写锁(Read-Write Lock)是什么?
14.【多线程】死锁
死锁是并发编程中一个非常经典且棘手的问题。它描述了一种特定的僵局状态,可以类比为一个现实生活中的场景:
一个生动的比喻:餐桌上的哲学家
想象一下,一张圆桌坐着五位哲学家,他们只做两件事:思考 和 吃饭。桌上只有五支筷子,每两位哲学家之间放一支。
规则是:
- 哲学家必须用左右两边的筷子才能吃饭。
- 他必须先拿起左边的筷子,再拿起右边的筷子。
- 如果有一支筷子被别人拿着,他就必须等待。
- 吃完后,他会同时放下两支筷子。
现在,考虑这样一种极端情况:
在某个时刻,所有五位哲学家同时决定要吃饭。他们每个人都按照规则,先拿起了自己左边的筷子。
结果就是:
- 现在每支筷子都被一个人拿着。
- 每个哲学家都在等待自己右边的筷子被释放。
- 但右边的筷子被自己右边的人拿着,而那个人也在等待他右边的筷子…
于是,所有哲学家都永远地等待下去,没有人能开始吃饭,也没有人会放下自己手中的筷子。 这就是死锁。
死锁的正式定义
在计算机科学中,死锁 是指两个或两个以上的并发进程(或线程),在彼此等待对方释放所占有的资源,但在没有外部干预的情况下,所有进程都无法继续推进的一种状态。
产生死锁的四个必要条件
死锁的发生必须同时满足以下四个条件,缺一不可:
-
互斥:一个资源每次只能被一个进程(或线程)使用。
- 就像一支筷子一次只能被一个哲学家使用。
-
持有并等待:一个进程在等待其他资源的同时,继续持有已分配到的资源。
- 就像每个哲学家都拿着左边的筷子(持有),同时在等待右边的筷子(等待)。
-
不可剥夺:进程已获得的资源,在未使用完之前,不能被强行剥夺。
- 你不能强行从哲学家手中抢走他已经拿起的筷子。
-
循环等待:存在一个进程资源的环形等待链。进程P1等待P2占有的资源,P2等待P3占有的资源,……,Pn等待P1占有的资源。
- 就像五位哲学家形成了一个等待的圆圈。
一个简单的代码示例
以下是一个极简的Java代码示例,演示了两个线程如何发生死锁:
public class SimpleDeadlock {private static final Object lock1 = new Object();private static final Object lock2 = new Object();public static void main(String[] args) {Thread threadA = new Thread(() -> {synchronized (lock1) { // 线程A获取了lock1System.out.println("Thread A: Holding lock 1...");try { Thread.sleep(10); } catch (InterruptedException e) {}System.out.println("Thread A: Waiting for lock 2...");synchronized (lock2) { // 线程A在等待lock2System.out.println("Thread A: Acquired both locks!");}}});Thread threadB = new Thread(() -> {synchronized (lock2) { // 线程B获取了lock2System.out.println("Thread B: Holding lock 2...");try { Thread.sleep(10); } catch (InterruptedException e) {}System.out.println("Thread B: Waiting for lock 1...");synchronized (lock1) { // 线程B在等待lock1System.out.println("Thread B: Acquired both locks!");}}});threadA.start();threadB.start();}
}
运行结果可能是:
Thread A: Holding lock 1...
Thread B: Holding lock 2...
Thread A: Waiting for lock 2...
Thread B: Waiting for lock 1...
// ... 然后程序就卡在这里,永远不会结束
分析:
- 线程A持有
lock1
,并请求lock2
。 - 线程B持有
lock2
,并请求lock1
。 - 它们互相等待对方释放锁,形成了循环等待,导致死锁。
如何处理和预防死锁?
既然死锁需要四个条件同时成立,那么破坏其中任意一个条件就可以预防死锁。
-
破坏“互斥”:
- 这通常很难,因为像打印机、数据库写操作等资源本质上是需要互斥的。但对于一些只读操作,可以设计为共享访问。
-
破坏“持有并等待”:
- 一次性申请所有资源:线程在开始执行前,必须一次性申请它所需要的所有资源。如果无法全部满足,它就什么资源都不占有,直接等待。这避免了在等待时还占着别的资源。
-
破坏“不可剥夺”:
- 如果一个线程已经持有了一些资源,但又无法申请到新的资源,那么它必须释放所有已占有的资源,以后需要时再重新申请。
-
破坏“循环等待”(最常用和实用的方法):
- 按顺序申请资源:给所有资源类型定义一个全局的线性顺序。每个线程都必须严格按照这个顺序来申请资源。
- 在上面的代码例子中,如果强制线程A和线程B都必须先申请
lock1
,再申请lock2
,那么死锁就不会发生。因为当线程B想申请lock1
时,它发现lock1
已经被A占用了,它就会被阻塞,并释放它已经占有的任何资源(在这个策略下,它此时不应该占有任何资源),从而打破了循环等待链。
总结
死锁是多线程编程中一个必须警惕的“陷阱”。它源于多个执行流对有限资源的竞争和不当的获取顺序。理解其产生的四个必要条件,并采取相应的预防策略(尤其是按顺序获取锁),是编写健壮、可靠的多线程程序的关键。在复杂系统中,还需要借助工具来检测和分析潜在的死锁。