16.5.6 死锁

16.5.6 死锁

什么时候会发生死锁

当两个线程相互等待对方释放同步监视器时就会发生死锁,

发生死锁会怎样

Java虚拟机没有监测,也没有采取措施来处理死锁情况,所以多线程编程时应该采取措施避免死锁出现。一旦出现死锁,整个程序既不会发生任何异常,也不会给出任何提示,只是所有线程处于阻塞状态,无法继续

程序 死锁示例

死锁是很容易发生的,尤其在系统中出现多个同步监视器的情况下,如下程序将会出现死锁。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
package thread;

class A
{
public synchronized void firstA(B b)
{
System.out.println(
Thread.currentThread().getName() + " 进入了A实例的firstA()方法 睡眠.zzz"); // ①
try
{
Thread.sleep(200);
} catch (InterruptedException ex)
{
ex.printStackTrace();
}
System.out.println(
Thread.currentThread().getName() + " 企图调用B实例的lastB()方法");
// ③
b.lastB();
}
public synchronized void lastA()
{
System.out.println("进入了A类的lastA()方法内部");
}
}
class B
{
public synchronized void firstB(A a)
{
System.out.println(
Thread.currentThread().getName() + " 进入了B实例的firstB()方法 睡眠.zzz"); // ②
try
{
Thread.sleep(200);
} catch (InterruptedException ex)
{
ex.printStackTrace();
}
System.out.println(
Thread.currentThread().getName() + " 企图调用A实例的lastA()方法"); // ④
a.lastA();
}
public synchronized void lastB()
{
System.out.println("进入了B类的lastB()方法内部");
}
}
public class DeadLock implements Runnable
{
A a = new A();
B b = new B();
public void init()
{
Thread.currentThread().setName("主线程");
// 调用a对象的firstA方法
a.firstA(b);
System.out.println("进入了主线程之后");
}
public void run()
{
Thread.currentThread().setName("副线程");
// 调用b对象的firstB方法
b.firstB(a);
System.out.println("进入了副线程之后");
}
public static void main(String[] args)
{
DeadLock dl = new DeadLock();
// 以dl为target启动新线程
new Thread(dl).start();
// 调用init()方法
dl.init();
}
}

运行上面程序,将会看到如下效果,此时程序既无法向下执行,也不会抛出任何异常,就一直”僵持”着

1
2
3
4
5
主线程 进入了A实例的firstA()方法 睡眠.zzz
副线程 进入了B实例的firstB()方法 睡眠.zzz
主线程 企图调用B实例的lastB()方法
副线程 企图调用A实例的lastA()方法
......一直阻塞......

究其原因,是因为上面程序中A对象和B对象的方法都是同步方法,也就是A对象和B对象都是同步锁
程序中两个线程执行:

  • 一个线程的线程执行体是DeadLock类的run()方法,
  • 另一个线程的线程执行体是DeadLockinit()方法(主线程调用了init()方法)。

其中run()方法中让B对象调用firstB()方法,
init()方法让A对象调用firstA()方法。

发生死锁的过程分析

  • 从本次运行结果来看,主线程的init()方法先执行,调用了A对象的firstA方法,进入firstA方法之前,该线程会对A对象加锁,不过当程序执行到firstA方法中的①号代码时,主线程睡眠200毫秒,在睡眠期间,主线程继续持有A对象的锁。
  • 这时候CPU切换到执行另一个线程(副线程),所以看到副线程开始执行B实例的firstB方法,进入firstB方法之前,该线程会对B对象加锁,不过当程序执行到firstB方法中的②号代码时,副线程也睡眠200毫秒,在睡眠期间,副线程继续持有B对象的锁。
  • 接下来主线程会先醒过来,继续向下执行,执行到③号代码处时,要调用B对象的lastB()方法,但执行该方法之前必须先对B对象加锁,由于此时副线程正保持着B对象的锁,所以主线程无法加锁,主线程阻塞;
  • 接下来副线程醒过来,继续向下执行,执行到④号代码处时,要调用A对象的lastA()方法,但执行该方法之前必须先对A对象加锁,由于主线程还没有释放A对象的锁,副线程无法加锁,副线程也阻塞
  • 这就出现了主线程保持着A对象的锁,等待对B对象加锁,而副线程保持着B对象的锁,等待对A对象加锁,两个线程互相等待对方先释放,所以就出现了死锁。

由于Thread类的suspend方法也很容易导致死锁,所以Java不再推荐使用该方法来暂停线程的执行

如何写一个死锁

  • 先要有两个线程,设为主线程,副线程
  • 要有两个类,设为A类对象和B类对象
    • A类对象有两个同步方法,
      • A类对象的第一个方法(设置firstA)的参数是B类对象,
        • 执行该方法时,先睡眠一会,然后调用B类对象的第二个无参同步方法lastB
      • A类对象的第二个同步方法lastB是个无参方法。
    • B类对象有两个同步方法,
      • B类对象的第一个方法firstB的参数传入A类对象,
        -firstB方法中调用A类对象的第二个无参同步方法lastA
      • B类对象的第二个方法同样是无参方法。
  • 创建A类对象,创建B类对象.
  • 主线程的执行体中调用A类对象的第一个方法firstA,该方法传入B类对象作为参数.
  • 副线程的执行题中条用B类对象的第一个方法firstB,该方法传入A类对象作为参数.
  • 启动这两个线程,就会发生死锁.