多线程 2 - 死锁问题

死锁

死锁,是多线程代码中的一类经典问题。加锁能够解决线程安全问题,但如果加锁方式不当,就很可能产生死锁。

出现死锁的三种场景

1、一个线程一把锁

就像上篇文章讲过的,如果对同一个线程上了两把锁,而且上的锁是不可重入锁的话,就会出现死锁问题。

2、两个线程两把锁

线程1获取锁A,同时线程2获取锁B,接下来,线程1尝试获取锁B,线程2尝试获取锁A。此时,就同样出现死锁了,一旦出现死锁,进程就会卡住,无法继续工作。死锁,是属于进程中的最严重的一种bug!!!

代码示例:

public class ThreadDemo22 {
    public static void main(String[] args) {
        Object A = new Object();
        Object B = new Object();
        Thread t1 = new Thread(()->{
            //sleep一下,是为了给t2时间,让t2也获取到锁
            synchronized (A){
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                //不释放A尝试获取B
                synchronized (B){
                    System.out.println("t1获得了两把锁");
                }
            }
        });
        Thread t2 = new Thread(()->{
        synchronized (B){
            //给t1时间,让t1也获取到锁
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            //尝试获取A,没有释放B
            synchronized (A){
                System.out.println("t2获得了两把锁");
            }
        }
        });
        t1.start();
        t2.start();
    }
}

点击运行,可以看到什么都没有打印:

通过jconsole进行观察 :

Thread-0被Thread-1锁住了,状态为BLOCKED;Thread-1也被Thread-0锁住了,状态为BLOCKED。

3、N个线程M把锁

这种情况就会涉及到“哲学家就餐”的问题。

有五个哲学家围坐在一张圆桌旁边,他们需要做的事就是思考和进餐。圆桌上有五根筷子,每两个哲学加之间放一根。当科学家需要进餐时,必须要同时拿起左右两边的筷子。如果筷子已经被其他科学家占用,那么当前要进餐的科学家必须等待,直到筷子可用。

也就是说,当某个哲学家在吃面条的过程中,他旁边的两位科学家,就需要进行阻塞等待(五个筷子就相当于五把锁,每个科学家,就相当于一个进程)。当线程拿到锁的时候,就会一直持有这把锁,除非它释放了锁(哲学家吃完了,主动放下了筷子)。在这个线程拿到这把锁的时候,其他线程不能获取到这把锁(哲学家都是有身份的人,不能硬抢筷子)。

虽然筷子数量并不充裕,但是,每个科学家,除了吃面条之外,还需要思考人生,在思考人生时,科学家是会放下筷子的。

由于每个哲学家,什么时候吃面条,什么时候思考人生,这个事情是不确定的(随机调度) ,绝大多数情况下,上述模型都是能够正常工作的。

但是,有一些极端的特殊情况,是无法正常工作的。

假设同一时刻,所有哲学家,都想吃面条,同时拿起了左手的筷子,这时,他们尝试拿右边的筷子。诶,发现拿不起来了(因为右边的筷子给别的科学家拿了)。

此时,由于所有的哲学家,都不想放下已经拿起来的筷子,就要等待旁边的人放下筷子,但是由于没有人吃到面,也就没有人放下筷子(当前每个线程锁里的任务还没有完成,也就没有线程释放锁,此时也就变成死锁了)。

产生死锁的四个必要条件

1、互斥使用:获取锁的过程是互斥的,一个线程拿到了这把锁,另一个线程也想获取就需要阻塞等待。

2、不可抢占:一个线程拿到锁之后,只能主动解锁,不能让别的线程,强行把锁抢走。

3、请求保持:一个线程在拿到A之后,在持有A的前提下,尝试获取B。

4、循环等待/环路等待: 就像上面的情况,五个哲学家同时想吃饭的时候,同时拿起了左边的筷子。

解决死锁问题

解决死锁问题就需要破坏上述四种必要条件的其中一个。

1、互斥使用。这是锁的基本特性,我们很难破坏。

2、不可抢占。这也是锁的特性,我们很难破坏。

3、请求保持。这个操作是取决于代码结构的,不一定可以破坏,要看实际代码需求。

4、循环等待/环路等待。这个是对比其他三个,最好破坏的。

我们可以通过指定加锁顺序,针对五把锁和五个哲学家,都进行编号,约定当每个哲学家(线程)拿筷子时候(获取锁的时候),必须要先获取编号小的筷子(锁),再获取编号大的筷子(锁)。

如果我们指定了加锁顺序,也就相当于指定了哲学家拿筷子的顺序,哲学家只能拿自己面前的两只筷子,且要先拿编号小的筷子。

那么,从2号哲学家开始,当他拿筷子的时候,眼前的1、2两只筷子,他需要先拿1筷子: 

2号哲学家拿完后,轮到3号哲学家,因为我们指定要拿编号小的筷子,所以他要拿的是2号筷子 同理,4、5号这学家要拿3、4号筷子。

但轮到1号哲学家时,因为我们指定要先拿编号小的筷子,即1号哲学家,要先获取1号筷子,但是此时1号筷子在2号哲学家手上,1号哲学家就只能等着了(阻塞等待)。此时5号筷子时空闲的,因此5号哲学家,就可以拿5号筷子,和4号筷子组成一双筷子吃面了!!!当5号哲学家吃完后,放下4、5号筷子,4号哲学家就可以吃面了,依此类推,3号、2号、1号哲学家都能吃上面了。

因此,对于刚才出现死锁的代码,我们只需要让t2线程先获取A锁再获取B锁就可以解决死锁问题了。

代码如下:


public class ThreadDemo22 {
    public static void main(String[] args) {
        Object A = new Object();
        Object B = new Object();
        Thread t1 = new Thread(()->{
            //sleep一下,是为了给t2时间,让t2也获取到锁
            synchronized (A){
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                //不释放A尝试获取B
                synchronized (B){
                    System.out.println("t1获得了两把锁");
                }
            }
        });
        Thread t2 = new Thread(()->{
        synchronized (A){
            //给t1时间,让t1也获取到锁
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            //尝试获取A,没有释放B
            synchronized (B){
                System.out.println("t2获得了两把锁");
            }
        }
        });
        t1.start();
        t2.start();
    }

运行结果: 

 那我们能不能指定编号小的哲学家先吃面呢?(编号小的线程先上锁)

这是不可行的,因为线程都是“随机调度”、“抢占式执行的”,想办法让某个线程先加锁,违背了“随机调度的原则”,可行性是不高的。而约定加锁顺序,在写代码的时候是非常容易做到的。

解决死锁的其他方法

1、引入额外的筷子:引入额外的锁。

2、去掉一个线程。

3、引入计数器,限制最多同时存在多少个线程。

上面三种方案,其实现并不难,但它们的适用性不高。

4、刚才讲过的引入加锁顺序的方法(普适性较高,且容易实现)。

5、学校的操作系统课中会有一种“银行家算法”,这个方案,确实可以可以解决死锁问题,但我们在实际开发中,一般不会这么做。因为这种方法实在太复杂了,为了解决死锁问题,实现“银行家算法”……死锁解没解决不确定,搞不好,我们在实现“银行家算法”的过程中,就bug满天飞了!!!(也就是说这种方法,理论上可行,实际中并不推荐)。

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值