Java并发编程(Java Concurrency)(17)- 预防死锁

原文链接:http://tutorials.jenkov.com/java-concurrency/deadlock-prevention.html

摘要:这是翻译自一个大概30个小节的关于Java并发编程的入门级教程,原作者Jakob Jenkov,译者Zhenning Lang,转载请注明出处,thanks and have a good time here~~~(希望自己不要留坑)

在一些情况下死锁的情况是可以被预防的,本节介绍三种相关的技术 —— 顺序锁(Lock Ordering)、超时锁(Lock Timeout)和死锁探测(Deadlock Detection)

1 顺序锁

死锁发生在多个线程都需要相同的(一系列)锁,但是所需要的顺序不同。如果可以确定任意的线程都以相同的顺序使用这些锁,那么死锁就不会发生。这种不同的线程间通以相同的顺序使用锁来预防死锁发生的办法就称为顺序锁。

请看下面的例子:

线程 1:
  锁 A 
  锁 B

线程 2:
   等待 A
   锁 C (当 A 被锁住)

线程 3:
   等待 A
   等待 B
   等待 C

如果一个线程,比如线程 3,需要几个锁,那么只要按照指定的顺序使用这些锁就不会发生死锁(译者理解:即线程 1、2、3 都是按照 A -> B -> C 的顺序获得锁,A、B、C 的顺序不会交换)。例如,线程 2 和 3 都不能在没有锁住 A 的时候锁住 C。因为线程 1 锁住了 A,线程 2 和 3 必须等到 A 先被解锁,然后线程 2 或 3 才能获得 A。

顺序锁是一个简单但高效的预防死锁的机制。但其前提是要预先知道所有锁的执行顺序,然而这有时是无法办到的。

2 超时锁

另一个阻止死锁的机制是在等待获得锁的时候加上一个时限,即一个线程只会尝试获取一个锁一段时间,随后就放弃,这被称为“超时锁”。如果时限内这个线程没能成功的获取其本想获得的全部的锁,这个线程就会备份并释放掉已经获得的全部锁资源,再等待一个随机的时长,然后重新从头开始尝试。等待随机时长的目的是让其他的需要相同锁的线程有机会获得锁,并使得程序继续运行下去。

下面的例子展示了两个线程以不同的顺序需要相同的两个锁,并且两个线程阻塞、备份、重试:

线程 1 锁住 A
线程 2 锁住 B

线程 1 尝试锁住 B 但阻塞住了
线程 2 尝试锁住 A 但阻塞住了

线程 1 超时
线程 1 备份并释放锁 A
线程 1 在重试前等待一段随机时间 (例如 257 毫秒)

线程 2 超时
线程 2 备份并释放锁 B
线程 2 在重试前等待一段随机时间 (例如 43 毫秒)

上面的例子中,线程 2 将比线程 1 早约 200ms ( 257ms - 43ms)尝试着获取锁 B 和锁 A,因此很可能成功获得全部的锁。线程 1 将等待获取锁 A,直到线程 2 完成全部工作后,线程 1 再按顺序获取锁 A 和锁 B。

值得注意的一点是,即便触发了超时,也不一定意味着发生了死锁,很可能只是一个线程所处理的任务非常费时。

另外一点是,即便引入超时的机制,如果足够多的线程竞争相同的锁,依旧会使得死锁的发生概率很高。两个线程每个线程随机等待 0-500ms 的例子可能不会发生死锁,但如果情况是 10 个乃至 100 个线程,即便随机等待了,两个线程同时开始执行的概率依旧很大。

想利用超时锁机制,就必须自己写一个 lock 类,或者利用 java.util.concurrency 包中的相关并发类。写一个新的 lock 类并不困难但超出了本文的讨论范畴,后面的章节内容会涉及到这部分内容。

3 死锁探测

“死锁探测”是一个重量级的死锁预防机制,适用于无法设计出顺序锁并且超时锁不适用的情况。

其具体的运作机制是这样的:每次一个线程实际获得一个锁时,那么就在某个数据结构(映射、图等,你可以自己定义)中做一个获得标记;每当一个线程需要获得一个锁时,也将在这个数据结构中做一个需求标记。如此一来,当一个线程请求一个锁但是无法获得时,这个线程就遍历这个数据结构来探测死锁。

例如,线程 A 需要锁 7,但是锁 7 正在被线程 B 使用(通过线程 B 的获得标记),此时线程 A 就可以检查线程 B 的需求标记,看线程 B 是否正在等待线程 A 正在使用的锁(通过线程 A 的获得标记)。如果情况就是如此,说明死锁发生了。

当然,实际的情况可能要远比上面的例子复杂。例如:线程 A 等待线程 B,线程 B 等待线程 C,线程 C 等待线程 D,线程 D 等待线程 A。那么线程 A 为了探测死锁,就必须先检测线程 B 所需求的锁,从而继续检测线程 C、线程 D 所需求的锁,最后才能判定死锁的发生。

四个线程的例子如下图所示,

Deadlock Detection Data Structure

那么如果探测到死锁,线程该如何应对?

一种可能的解决方案是所有的线程释放掉所有的锁,每个等待一个随机的时长然后重新尝试。这类似于前面介绍的超时锁机制,只不过这里不再利用超时,而是实实在在探测到了死锁的发生。然而,类似于前面介绍的内容,对于很多线程的情况,不停的重试依旧会有很高的概率诱发死锁。

一个更好的选择是为不同的线程分配优先级,使得只有一个或很少的线程释放掉其持有的锁资源,而其他的线程正常运作。但是如果各个线程间的优先级是确定的话,相同的线程可能总是获得较低的优先级而无法被执行(见后面章节中对“饥饿”的介绍)。为了避免这一情况发生,需要在死锁发生时随机分配优先级。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值