Java线程死锁的原因和解决方法(附带实例)
死锁是指两个或两个以上的线程在执行过程中,因争夺资源而造成的互相等待的现象,在无外力作用的情况下,这些线程会一直相互等待而无法继续运行下去。
如下图所示的死锁状态,线程 A 己经持有了资源 1,它同时还想申请资源 2,可是此时线程 B 已经持有了资源 2,线程 A 只能等待。

图 1 死锁状态
反观线程 B 持有了资源 2,它同时还想申请资源 1,但是资源 1 已经被线程 A 持有,线程 B 只能等待。所以线程 A 和线程 B 就因为相互等待对方已经持有的资源,而进入了死锁状态。
那么,线程死锁的必备要素都有哪些呢?具体如下:

图 2 循环等待条件
【实例 1】死锁的实现。创建两个线程,名称分别为 threadA 和 threadB;创建两个资源(使用 newObject() 创建即可),名称分别为 resourceA 和 resourceB:
为了确保发生死锁现象,请使用 sleep() 方法创造该场景;执行代码,看是否会发生死锁,即线程 threadA 和 threadB 互相等待。代码如下:
下面修改例 1,在其他条件保持不变的情况下,仅对之前的 threadB 的代码做如下修改,以避免死锁。代码如下:
综上,threadA 和 threadB 按照相同的顺序对 resourceA 和 resourceB 依次进行访问,避免了互相交叉持有等待的状态,因而避免了死锁的发生。
如下图所示的死锁状态,线程 A 己经持有了资源 1,它同时还想申请资源 2,可是此时线程 B 已经持有了资源 2,线程 A 只能等待。

图 1 死锁状态
反观线程 B 持有了资源 2,它同时还想申请资源 1,但是资源 1 已经被线程 A 持有,线程 B 只能等待。所以线程 A 和线程 B 就因为相互等待对方已经持有的资源,而进入了死锁状态。
那么,线程死锁的必备要素都有哪些呢?具体如下:
- 互斥条件:进程要求对所分配的资源进行排他性控制,即在一段时间内某资源仅为一个进程所占有。此时若有其他进程请求该资源,则请求进程只能等待。
- 不可剥夺条件:进程所获得的资源在未使用完毕之前,不能被其他进程强行夺走,即只能由获得该资源的进程自己来释放(只能是主动释放,如 yield 释放 CPU 执行权)。
- 请求与保持条件:进程已经保持了至少一个资源,但又提出了新的资源请求,而该资源已被其他进程占有,此时请求进程被阻塞,但对自己已获得的资源保持不放。
- 循环等待条件:指在发生死锁时,必然存在一个如图 3 所示的、线程请求资源的环形链,即线程集合{T0,T1,T2,…,Tn}中的 T0 正在等待一个 T1 占用的资源,T1 正在等待 T2 占用的资源,以此类推,Tn 正在等待己被 T0 占用的资源。

图 2 循环等待条件
【实例 1】死锁的实现。创建两个线程,名称分别为 threadA 和 threadB;创建两个资源(使用 newObject() 创建即可),名称分别为 resourceA 和 resourceB:
- threadA 持有 resourceA 并申请资源 resourceB;
- threadB 持有 resourceB 并申请资源 resourceA。
为了确保发生死锁现象,请使用 sleep() 方法创造该场景;执行代码,看是否会发生死锁,即线程 threadA 和 threadB 互相等待。代码如下:
public class Demo { private static Object resourceA = new Object(); // 创建资源 resourceA private static Object resourceB = new Object(); // 创建资源 resourceB public static void main(String[] args) throws InterruptedException { // 创建线程 threadA Thread threadA = new Thread(new Runnable() { @Override public void run() { synchronized (resourceA) { System.out.println(Thread.currentThread().getName() + "获取 resourceA。"); try { // sleep 1000 毫秒,确保此时 resourceB 已经进入 run()方法的同步模块中 Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "开始申请 resourceB。"); synchronized (resourceB) { System.out.println(Thread.currentThread().getName() + "获取 resourceB。"); } } } }); threadA.setName("threadA"); // 创建线程 threadB Thread threadB = new Thread(new Runnable() { @Override public void run() { synchronized (resourceB) { System.out.println(Thread.currentThread().getName() + "获取 resourceB。"); try { // sleep 1000 毫秒,确保此时 resourceA 已经进入 run()方法的同步模块中 Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "开始申请 resourceA。"); synchronized (resourceA) { System.out.println(Thread.currentThread().getName() + "获取 resourceA。"); } } } }); threadB.setName("threadB"); threadA.start(); threadB.start(); } }运行结果为:
threadB获取 resourceB。
threadA获取 resourceA。
threadA开始申请 resourceB。
threadB开始申请 resourceA。
- threadA 首先获取了 resourceA,获取的方式是代码 synchronized(resourceA),然后沉睡 1000 毫秒;
- 在 threadA 沉睡过程中,threadB 获取了 resourceB,然后使自己沉睡 1000 毫秒;
- 当两个线程都苏醒时,此时可以确定 threadA 获取了 resourceA,threadB 获取了 resourceB,这就达到了我们做的第一步,线程分别持有自己的资源;
- 开始申请资源,threadA 申请资源 resourceB,threadB 申请资源 resourceA,无奈 resourceA 和 resourceB 都被各自线程持有,两个线程均无法申请成功,最终达成死锁状态。
避免死锁的方法
要想避免死锁,只需要破坏掉至少一个构造死锁的必要条件即可,学过操作系统的读者应该都知道,目前只有请求并持有和环路等待条件是可以被破坏的。造成死锁的原因其实和申请资源的顺序有很大关系,使用资源申请的有序性原则就可避免死锁。下面修改例 1,在其他条件保持不变的情况下,仅对之前的 threadB 的代码做如下修改,以避免死锁。代码如下:
Thread threadB = new Thread(new Runnable() { @Override public void run() { synchronized (resourceA) { System.out.println(Thread.currentThread().getName() + "获取 resourceA。"); try { // sleep 1000 毫秒,确保此时 resourceA 已经进入 run()方法的同步模块中 Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "开始申请 resourceA。"); } synchronized (resourceB) { System.out.println(Thread.currentThread().getName() + "获取 resourceA。"); } } });运行结果为:
threadB获取 resourceB。
threadB开始申请 resourceA。
threadB获取 resourceA。
threadA获取 resourceA。
threadA开始申请 resourceB。
threadA获取 resourceB。
- threadA 首先获取了 resourceA,获取的方式是代码 synchronized(resourceA),然后沉睡 1000 毫秒;
- 在 threadA 沉睡过程中,threadB 想要获取 resourceA,但是 resourceA 目前正被沉睡的 threadA 持有,因此 threadB 等待 threadA 释放 resourceA;
- 1000 毫秒后,threadA 苏醒了,释放了 resourceA,此时等待的 threadB 获取到了 resourceA,然后 threadB 使自己沉睡 1000 毫秒;
- threadB 沉睡过程中,threadA 申请 resourceB 成功,继续执行成功后,释放 resourceB;
- 1000 毫秒后,threadB 苏醒了,继续执行获取 resourceB,执行成功。
综上,threadA 和 threadB 按照相同的顺序对 resourceA 和 resourceB 依次进行访问,避免了互相交叉持有等待的状态,因而避免了死锁的发生。