Java中的线程死锁问题(附带实例)
所谓死锁,是指多个线程因竞争资源而造成的一种僵局(互相等待),如果无外力作用,那么这些进程都将无法向前推进。
线程死锁的示意图如下图所示:

图 1 线程死锁的示意图
环路等待的示意图如下图所示:

图 2 环路等待的示意图
当上述 4 个条件都成立时,便形成死锁。当然,在发生死锁的情况下,如果打破上述任何一个条件,就可以让死锁消失。
【实例】一个简单的产生死锁的应用。
在操作系统中,互斥条件和不可剥夺条件是操作系统规定的,没有办法人为更改,并且这两个条件明显是一个标准的程序应该具备的特性。
所以,目前只有请求并持有条件和环路等待条件是可以被破坏的。
产生死锁的原因其实和申请资源的顺序有很大的关系。使用资源申请的有序性原则就可以避免产生死锁。因此,解决死锁的方法有两种:
【实例】解决上面实例中产生的死锁。
td1 和 td2 同时执行了 synchronized(o1),只有一个线程可以获取到 o1 锁上的监听器锁,假如 td1 获取到了,那么 td2 就会被阻塞而不会再获取 o2 锁,td1 获取到 o1 锁的监听器锁之后会申请 o2 锁的监听器锁,这时 td1 是可以获取到的,td1 获取到 o2 锁并使用后先释放 o2 锁,再释放 o1 锁,释放 o1 锁之后 td2 才会从阻塞状态变为运行状态。所以,资源的有序性破坏了资源的请求并持有条件和环路等待条件,由此可以避免产生死锁。
线程死锁的示意图如下图所示:

图 1 线程死锁的示意图
产生死锁的原因
死锁主要是由以下 4 个因素造成的:- 互斥条件:是指线程对已经获取到的资源进行排他性使用,即该资源同时只由一个线程占用。如果此时还有其他线程请求获取该资源,那么请求者只能等待,直到占用资源的线程释放该资源。
- 不可被剥夺条件:是指线程获取到的资源在自己使用完之前不能被其他线程占用,只有在自己使用完毕才由自己释放该资源。
- 请求并持有条件:是指一个线程已经占用了至少一个资源,但又提出了新的资源请求,而新的资源已被其他线程占用,所以当前线程会被阻塞,但阻塞的同时并不释放自己已经获取的资源。
- 环路等待条件:是指在发生死锁时,必然存在一个(线程—资源)环形链,即线程集合 {T0,T1,T2,…,Tn} 中的 T0 正在等待 T1 占用的资源,T1 正在等待 T2 占用的资源,依次类推,Tn 正在等待 T0 占用的资源。
环路等待的示意图如下图所示:

图 2 环路等待的示意图
当上述 4 个条件都成立时,便形成死锁。当然,在发生死锁的情况下,如果打破上述任何一个条件,就可以让死锁消失。
【实例】一个简单的产生死锁的应用。
public class Example { public static void main(String[] args) { DeadDemo td1 = new DeadDemo(); DeadDemo td2 = new DeadDemo(); td1.flag = 1; td2.flag = 0; new Thread(td1, "td1").start(); new Thread(td2, "td2").start(); } } class DeadDemo implements Runnable { public int flag = 1; // 静态对象由类的所有对象共享 private static Object o1 = new Object(), o2 = new Object(); @Override public void run() { String threadName = Thread.currentThread().getName(); System.out.println(threadName + ": flag = " + flag); if (flag == 1) { synchronized (o1) { System.out.println(threadName + ": 取得 o1 锁"); try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(threadName + ": 申请 o2 锁"); synchronized (o2) { System.out.println("1"); } } } if (flag == 0) { synchronized (o2) { System.out.println(threadName + ": 取得 o2 锁"); try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(threadName + ": 申请 o1 锁"); synchronized (o1) { System.out.println("0"); } } } } }运行结果为:
td2: flag=0
td1: flag=1
td2: 取得o2锁
td1: 取得o1锁
td1: 申请o2锁
td2: 申请o1锁
- td1 睡眠结束后需要锁定 o2 锁才能继续执行,而此时 o2 锁已被 td2 锁定;
- td2 睡眠结束后需要锁定 o1 锁才能继续执行,而此时 o1 锁已被 td1 锁定;
- td1 和 td2 相互等待,都需要得到对方锁定的资源才能继续执行,从而造成死锁。
解决死锁的方法
要想解决死锁,只需要破坏至少一个产生死锁的必要条件即可。在操作系统中,互斥条件和不可剥夺条件是操作系统规定的,没有办法人为更改,并且这两个条件明显是一个标准的程序应该具备的特性。
所以,目前只有请求并持有条件和环路等待条件是可以被破坏的。
产生死锁的原因其实和申请资源的顺序有很大的关系。使用资源申请的有序性原则就可以避免产生死锁。因此,解决死锁的方法有两种:
- 设置加锁顺序(线程按照一定的顺序加锁);
- 设置加锁时限(线程获取锁的时候加上一定的时限,若超过时限则放弃对该锁的请求,并释放自己占用的锁)。
【实例】解决上面实例中产生的死锁。
public class Example { public static void main(String[] args) { UnDeadDemo td1 = new UnDeadDemo(); UnDeadDemo td2 = new UnDeadDemo(); td1.flag = 1; td2.flag = 0; new Thread(td1, "td1").start(); new Thread(td2, "td2").start(); } } class UnDeadDemo implements Runnable { public int flag = 1; // 静态对象由类的所有对象共享 private static Object o1 = new Object(), o2 = new Object(); @Override public void run() { String threadName = Thread.currentThread().getName(); System.out.println(threadName + ": flag = " + flag); if (flag == 1) { synchronized (o1) { System.out.println(threadName + ": 取得 o1 锁"); try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(threadName + ": 申请 o2 锁"); synchronized (o2) { System.out.println("1"); } } } if (flag == 0) { synchronized (o2) { System.out.println(threadName + ": 取得 o2 锁"); try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(threadName + ": 申请 o1 锁"); synchronized (o1) { System.out.println("0"); } } } } }运行结果为:
td2: flag=0
td1: flag=1
td2: 取得o2锁
td2: 申请o1锁
0
td1: 取得o1锁
td1: 申请o2锁
1
td1 和 td2 同时执行了 synchronized(o1),只有一个线程可以获取到 o1 锁上的监听器锁,假如 td1 获取到了,那么 td2 就会被阻塞而不会再获取 o2 锁,td1 获取到 o1 锁的监听器锁之后会申请 o2 锁的监听器锁,这时 td1 是可以获取到的,td1 获取到 o2 锁并使用后先释放 o2 锁,再释放 o1 锁,释放 o1 锁之后 td2 才会从阻塞状态变为运行状态。所以,资源的有序性破坏了资源的请求并持有条件和环路等待条件,由此可以避免产生死锁。