什么是线程死锁
线程死锁是指由于两个或者多个线程互相持有对方所需要的资源,导致这些线程处于等待状态,无法前往执行。当线程进入对象的 synchronized 代码块时,便占有了资源,直到它退出该代码块或者调用 wait 方法,才释放资源,在此期间,其他线程将不能进入该代码块。当线程互相持有对方所需要的资源时,会互相等待对方释放资源,如果线程都不主动释放所占有的资源,将产生死锁。
场景
场景一:
星期日早上十点半,你在公路上开车,这是一条窄路,只能容纳一辆车。这时,迎面又驶来一辆车,你们都走到一半,谁也不想倒回去,于是各不相让,陷入无尽的等待。
场景二:
你有两个线程 A 和 B ,各自在加锁的状态下运行。A 持有一部分资源,并且等待 B 线程中的资源以完成自己的工作,而此时 B 线程也在等待 A 中的资源以完成自己的工作。由于他们都是锁定状态,所以他们必须完成了自己的工作后,自己持有的资源才能释放。于是线程无休止地等待,导致死锁。
1 | public class JavaTest { |
2 | |
3 | public void test() { |
4 | final Object lockA = new Object(); |
5 | final Object lockB = new Object(); |
6 | new Thread(new Runnable() { |
7 | |
8 | public void run() { |
9 | synchronized (lockA) { |
10 | try { |
11 | Thread.sleep(1000); |
12 | } catch (InterruptedException e) { |
13 | e.printStackTrace(); |
14 | } |
15 | synchronized (lockB) { |
16 | } |
17 | System.out.println("finish A"); |
18 | } |
19 | } |
20 | }).start(); |
21 | new Thread(new Runnable() { |
22 | |
23 | public void run() { |
24 | synchronized (lockB) { |
25 | try { |
26 | Thread.sleep(1000); |
27 | } catch (InterruptedException e) { |
28 | e.printStackTrace(); |
29 | } |
30 | synchronized (lockA) { |
31 | } |
32 | System.out.println("finish B"); |
33 | } |
34 | } |
35 | }).start(); |
36 | try { |
37 | Thread.sleep(10000); |
38 | } catch (InterruptedException e) { |
39 | e.printStackTrace(); |
40 | } |
41 | } |
42 | } |
此程序中,线程 A 持有 lockA 对象,并请求 lockB 对象;线程 B 持有 lockB 对象,并请求 lockA 对象。由于他们都在等待对方释放资源,所以会产生死锁。运行程序,将发现控制台无法打印出 “finish A” 和 “finish B” 消息。
产生死锁的四个条件
- 互斥条件:一个资源每次只能被一个进程使用;
- 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放;
- 不可抢占条件:进程已获得的资源,在没使用完之前,不能强行剥夺;
- 循环等待条件:多个进程之间形成一种互相循环等待资源的关系。
如何预防线程死锁
预防死锁的方法是通过破坏死锁的四个必要条件中的一个或几个,以避免发生死锁。
破坏不可抢占条件:让对面的司机放弃了自己已有的资源。
破坏请求与保持条件:在自己需要的材料缺少时,主动放弃自己持有的资源,防止出现互相等待。
破坏循环等待条件:由于筷子指定了编号和获取规则,所以每个锁定状态都将按照顺序执行,于是便杜绝了环路等待条件。
破坏互斥条件:由于每次使用时都拷贝一份,所以一个资源可以被多个进程使用。
如何避免线程死锁
在死锁避免方法中,系统有两种状态:安全状态和不安全状态;当系统处于安全状态时,可以避免进入死锁;当系统处于不安全状态时,有可能进入死锁;避免死锁的基本思想就是确保系统始终处于安全状态。
当有进程请求一个可用资源时,系统需要对该进程的请求进行计算,如果将资源分配给进程之后,系统仍然处于安全状态,那么该资源才可以分配给进程;否则不进行分配;
避免死锁的最有代表性的算法就是艾兹格·迪杰斯特拉在 1965 年设计的银行家算法,通过记录系统中的资源向量、最大需求矩阵、分配矩阵、需求矩阵,以保证系统只在安全状态下进行资源分配,由此来避免死锁。
死锁的检测和接触
死锁的检测
为实现死锁检测算法,需要系统记录各种资源的使用情况以及进程对资源的请求情况,并且系统需要能根据这些信息检测系统是否进入死锁状态;一种常见的死锁检测方法是利用资源分配图。
死锁的解除
死锁解除常使用两种方法:抢占资源和终止进程;
一种最小代价的死锁解除算法是:对于死锁状态的每一个进程,都计算一遍终止该进程所产生的代价,然后选择代价最小的一个进程终止,然后如果系统还处于死锁,那么重复前一个步骤;但是计算选择代价最小的进程这一步骤可能会有较大的代价!
参考: