什么是线程死锁

死锁是指两个或两个以上的线程在执行过程中,因争夺资源而造成的互相等待的现象,在无外力作用的情况下,这些线程会一直相互等待而无法继续运行下去,如图 1-2 所示。

线程死锁 - 图1

图 1-2

在图 1-2 中,线程 A 已经持有了资源 2,它同时还想申请资源 1,线程 B 已经持有了资源 1,它同时还想申请资源 2,所以线程 1 和线程 2 就因为相互等待对方已经持有的资源,而进入了死锁状态。

那么为什么会产生死锁呢?学过操作系统的朋友应该都知道,死锁的产生必须具备以下四个条件。

● 互斥条件:指线程对已经获取到的资源进行排它性使用,即该资源同时只由一个线程占用。如果此时还有其他线程请求获取该资源,则请求者只能等待,直至占有资源的线程释放该资源。

● 请求并持有条件:指一个线程已经持有了至少一个资源,但又提出了新的资源请求,而新资源已被其他线程占有,所以当前线程会被阻塞,但阻塞的同时并不释放自己已经获取的资源。

● 不可剥夺条件:指线程获取到的资源在自己使用完之前不能被其他线程抢占,只有在自己使用完毕后才由自己释放该资源。

● 环路等待条件:指在发生死锁时,必然存在一个线程—资源的环形链,即线程集合{T0,T1,T2,…,Tn}中的 T0 正在等待一个 T1 占用的资源,T1 正在等待 T2 占用的资源,……Tn正在等待已被 T0 占用的资源。

下面通过一个例子来说明线程死锁。

  1. public class DeadLockTest2 {
  2. // 创建资源
  3. private static Object resourceA = new Object();
  4. private static Object resourceB = new Object();
  5. public static void mainString[] args {
  6. // 创建线程 A
  7. Thread threadA = new Thread(new Runnable() {
  8. public void run() {
  9. synchronized resourceA {
  10. System.out.println(Thread.currentThread() + get ResourceA」);
  11. try {
  12. Thread.sleep(1000);
  13. } catch InterruptedException e {
  14. e.printStackTrace();
  15. }
  16. System.out.println(Thread.currentThread() + waiting get sourceB」);
  17. synchronized resourceB {
  18. System.out.println(Thread.currentThread() + get esourceB」);
  19. }
  20. }
  21. }
  22. });
  23. // 创建线程 B
  24. Thread threadB = new Thread(new Runnable() {
  25. public void run() {
  26. synchronized resourceB {
  27. System.out.println(Thread.currentThread() + get ResourceB」);
  28. try {
  29. Thread.sleep(1000);
  30. } catch InterruptedException e {
  31. e.printStackTrace();
  32. }
  33. System.out.println(Thread.currentThread() + waiting get esourceA」);
  34. synchronized resourceA {
  35. System.out.println(Thread.currentThread() + get ResourceA」);
  36. }
  37. };
  38. }
  39. });
  40. // 启动线程
  41. threadA.start();
  42. threadB.start();
  43. }
  44. }

输出结果如下。

线程死锁 - 图2

下面分析代码和结果:Thread-0 是线程 A,Thread-1 是线程 B,代码首先创建了两个资源,并创建了两个线程。从输出结果可以知道,线程调度器先调度了线程 A,也就是把 CPU 资源分配给了线程 A,线程 A 使用 synchronized(resourceA)方法获取到了 resourceA 的监视器锁,然后调用 sleep 函数休眠 1s,休眠 1s 是为了保证线程 A 在获取 resourceB 对应的锁前让线程 B 抢占到 CPU,获取到资源 resourceB 上的锁。线程 A 调用 sleep 方法后线程 B 会执行 synchronized(resourceB)方法,这代表线程 B 获取到了 resourceB 对象的监视器锁资源,然后调用 sleep 函数休眠 1s。好了,到了这里线程 A 获取到了 resourceA 资源,线程 B 获取到了 resourceB 资源。线程 A 休眠结束后会企图获取 resourceB 资源,而 resourceB 资源被线程 B 所持有,所以线程 A 会被阻塞而等待。而同时线程 B 休眠结束后会企图获取 resourceA 资源,而 resourceA 资源已经被线程 A 持有,所以线程 A 和线程 B 就陷入了相互等待的状态,也就产生了死锁。下面谈谈本例是如何满足死锁的四个条件的。

首先,resourceA 和 resourceB 都是互斥资源,当线程 A 调用 synchronized(resourceA)方法获取到 resourceA 上的监视器锁并释放前,线程 B 再调用 synchronized(resourceA)方法尝试获取该资源会被阻塞,只有线程 A 主动释放该锁,线程 B 才能获得,这满足了资源互斥条件。

线程 A 首先通过 synchronized(resourceA)方法获取到 resourceA 上的监视器锁资源,然后通过 synchronized(resourceB)方法等待获取 resourceB 上的监视器锁资源,这就构成了请求并持有条件。

线程 A 在获取 resourceA 上的监视器锁资源后,该资源不会被线程 B 掠夺走,只有线程 A 自己主动释放 resourceA 资源时,它才会放弃对该资源的持有权,这构成了资源的不可剥夺条件。

线程 A 持有 objectA 资源并等待获取 objectB 资源,而线程 B 持有 objectB 资源并等待 objectA 资源,这构成了环路等待条件。所以线程 A 和线程 B 就进入了死锁状态。

如何避免线程死锁

要想避免死锁,只需要破坏掉至少一个构造死锁的必要条件即可,但是学过操作系统的读者应该都知道,目前只有请求并持有和环路等待条件是可以被破坏的。

造成死锁的原因其实和申请资源的顺序有很大关系,使用资源申请的有序性原则就可以避免死锁,那么什么是资源申请的有序性呢?我们对上面线程 B 的代码进行如下修改。

  1. // 创建线程 B
  2. Thread threadB = new Thread(new Runnable() {
  3. public void run() {
  4. synchronized resourceA {
  5. System.out.println(Thread.currentThread() + get ResourceB」);
  6. try {
  7. Thread.sleep(1000);
  8. } catch InterruptedException e {
  9. e.printStackTrace();
  10. }
  11. System.out.println(Thread.currentThread() + waiting get ResourceA」);
  12. synchronized resourceB {
  13. System.out.println(Thread.currentThread() + get ResourceA」);
  14. }
  15. };
  16. }
  17. });

输出结果如下。

线程死锁 - 图3

如上代码让在线程 B 中获取资源的顺序和在线程 A 中获取资源的顺序保持一致,其实资源分配有序性就是指,假如线程 A 和线程 B 都需要资源 1,2,3,…,n时,对资源进行排序,线程 A 和线程 B 只有在获取了资源n-_1 时才能去获取资源_n

我们可以简单分析一下为何资源的有序分配会避免死锁,比如上面的代码,假如线程 A 和线程 B 同时执行到了 synchronized(resourceA),只有一个线程可以获取到 resourceA 上的监视器锁,假如线程 A 获取到了,那么线程 B 就会被阻塞而不会再去获取资源 B,线程 A 获取到 resourceA 的监视器锁后会去申请 resourceB 的监视器锁资源,这时候线程 A 是可以获取到的,线程 A 获取到 resourceB 资源并使用后会放弃对资源 resourceB 的持有,然后再释放对 resourceA 的持有,释放 resourceA 后线程 B 才会被从阻塞状态变为激活状态。所以资源的有序性破坏了资源的请求并持有条件和环路等待条件,因此避免了死锁。