Java并发编程之线程死锁

279 阅读4分钟

这是我参与11月更文挑战的第10天,活动详情查看:2021最后一次更文挑战

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

线程产生死锁的四个条件:

  • 互斥条件:指线程对已经获取到的资源进行排他性使用,即该资源同时只有同时只由一个线程占用。如果此时还有其他线程请求获取该资源,则请求者只能等待,直至占有资源的线程释放该资源。
  • 请求并持有条件:指一个线程已经持有了至少一个资源,但又提出了新的资源请求,而新资源已被其他线程占有,所以当前线程会被阻塞,但阻塞的同时并不释放自己已经获取的资源。
  • 不可剥夺条件:指线程获取到的资源在自己使用完之前不能被其他线程抢占,只有在自己使用完毕后才由自己释放的资源。
  • 环路等待条件:指在发生死锁时,必然存在一个 线程—资源的环形链,即线程集合{T0,T1,T2,···,Tn}中的T0正在等待一个T1占用的资源,T1正在等待T2占用的资源,···Tn正在等待已被T0占用的资源。
public class code_1_9_DeadLockTest {

    //创建资源
    private static Object resourceA = new Object();
    private static Object resourceB = new Object();

    public static void main(String[] args) throws InterruptedException{
        //创建线程
        Thread threadA = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (resourceA) {
                    System.out.println(Thread.currentThread() + " get ResourceA");

                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread() + " waiting get sourceB");
                    synchronized (resourceB) {
                        System.out.println(Thread.currentThread() + " get resourceB");
                    }
                }
            }
        });

        Thread threadB = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (resourceB) {
                    System.out.println(Thread.currentThread() + " get ResourceB");

                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread() + " waiting get sourceA");
                    synchronized (resourceA) {
                        System.out.println(Thread.currentThread() + " get resourceA");
                    }
                }
            }
        });
        //启动线程
        threadA.start();
        threadB.start();
    }
}

输出结果如下:

image.png

上述代码中,首先创建了两个资源resourceA和resourceB。线程调度器先调度了线程A,线程A使用synchronized获取了resourceA,然后休眠1s,保证了线程B先获取到resourceB。线程A会尝试获取resourceB,而线程B会尝试获取resourceA。所以线程A和线程B陷入了相互等待状态,产生了死锁。

那么本例子是怎么满足死锁的四个必要条件的呢?

  1. resourceA和resourceB都是互斥资源,只用当前持有资源的线程释放掉后,另一线程才能持有。这就满足了资源互斥条件了。

  2. 线程A持有了resourceA,然后继续等待获取resourceB,这就构成了请求并持有条件。

  3. 线程A持有resourceA之后,该资源不可以被线程B抢占走,只有线程A自己释放资源时,它才会放弃对资源的持有权,这构成了资源不可剥夺条件。

  4. 线程A持有resourceA,等待获取resourceB,而线程B持有resourceB,等待获取resourceA,这就构成了环路等待条件。

那么应该怎么避免死锁了?

想要避免死锁,我们只需要破坏一个构成死锁的必要条件。然而目前只有请求并持有和环路等待条件时可以被破坏的。造成死锁的原因其实和申请资源的顺序有很大关系,使用资源申请的有序性原则就可以避免死锁。 例如我们将上面代码中线程B申请资源的顺序改为和线程A一样。

        Thread threadB = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (resourceA) {
                    System.out.println(Thread.currentThread() + " get ResourceA");

                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread() + " waiting get sourceB");
                    synchronized (resourceB) {
                        System.out.println(Thread.currentThread() + " get resourceB");
                    }
                }
            }
        });

输出如下:

image.png

其实资源分配的有序性就是指,假设线程A和线程B都需要资源A,B,C···时,对资源进行排序,线程A和线程B只有在获取了资源n-1时才能去获取资源n。 在修改后的代码中,加入线程A获取到了resourceA,那么线程B就会阻塞,而不会去获取resourceB。这时候线程A可以获取resourceB的监视器锁资源,获取后放弃对resourceA的持有,这时候,线程B可以获取resourceA。