并发编程(六)死锁

死锁是指多个线程因争夺资源形成的一种互相等待的状态,无法继续执行。产生死锁需满足互斥、请求并持有、不可剥夺和环路等待四个条件。通过破坏这四个条件之一,特别是采用资源申请的有序性,可以避免死锁问题。示例代码展示了如何通过调整资源获取顺序来消除死锁。
摘要由CSDN通过智能技术生成

死锁

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

在这里插入图片描述

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

那么为什么会产生死锁呢?

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

● 互斥条件

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

● 请求并持有条件

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

● 不可剥夺条件

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

● 环路等待条件

指在发生死锁时,必然存在一个线程—资源的环形链,即线程集合{T0, T1,T2,…,Tn}中的T0正在等待一个T1占用的资源,T1正在等待T2占用的资源,……Tn正在等待已被T0占用的资源。下面通过一个例子来说明线程死锁。

死锁代码示例
public class DeadLockTest2 {
    // 创建资源
    private static Object resourceA = new Object();
    private static Object resourceB = new Object();
    public static void main(String[] args) {
        // 创建线程A
        Thread threadA = new Thread(new Runnable() {
            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 esourceB");
                    }
                }
            }
        });
        // 创建线程B
        Thread threadB = new Thread(new Runnable() {
            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 esourceA");
                    synchronized (resourceA) {
                        System.out.println(Thread.currentThread() + "get ResourceA");
                    }
                };
            }
        });
        // 启动线程
        threadA.start();
        threadB.start();
    }
}
代码分析和结果

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的代码进行如下修改。

// 创建线程B
    Thread threadB = new Thread(new Runnable() {
        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 ResourceA");
                synchronized (resourceB) {
                    System.out.println(Thread.currentThread() + "get ResourceB");
                }
            };
        }
    });

输出结果如下。
在这里插入图片描述

如上代码让在线程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才会被从阻塞状态变为激活状态。所以资源的有序性破坏了资源的请求并持有条件和环路等待条件,因此避免了死锁。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值