多线程——“死锁”

目录

·前言

一、一个线程,一把锁

1.问题介绍

2.可重入锁 

二、两个线程,两把锁

1.问题介绍

2.解决方式

三、N个线程,M把锁

1.哲学家就餐问题

2.解决方式

·结尾


·前言

        “死锁”是多线程代码中一类常见的问题,加锁是能解决线程安全的问题,但是如果加锁的方式不当,就可能产生“死锁”,本篇文章就会对“死锁”的三个比较常见的场景进行介绍。

一、一个线程,一把锁

1.问题介绍

        当你在编程过程中,使用的锁是不可重入锁,并且在同一个线程中对同一把锁进行两次加锁,此时就可能面临“死锁”的状况,代码如下,只不过在Java语言中,锁都是可重入锁,所以并不能演示出“死锁”的效果,不过相关的逻辑可以用Java代码进行表示,如下图所示:

        上面这段代码,如果锁是不可重入锁就会出现“死锁”,因为第二次对locker进行加锁时由于locker还没有被第一次加锁操作释放所以会进入阻塞等待,然而想要释放locker必须执行完第一个synchronized中的代码,此时就会进入“死锁”,也就是卡住,利用生活中的例子就像你把钥匙锁在了屋里一样,想要开门就需要钥匙,可是钥匙确被门锁了起来。此时想要解决这种情况,很简单,不在同一个线程中对同一把锁重复加两次就好了,或者使用可重入锁,都可以解决这种问题。

2.可重入锁 

        Java中的锁是可重入锁,在这里再对可重入锁进行一个简单介绍。

        对于可重入锁来说,它的内部会持有两个信息:

  1. 当前这个锁是哪个线程持有的
  2. 记录加锁次数的计数器

        下面再利用上面演示的代码,对Java中可重入锁的执行过程进行详细介绍:         在上述对同一把锁进行第二次加锁时,会先判断当前加锁的线程是否是持有锁的线程,如果不是同一个线程,那么就会进行阻塞,如果是同一个线程就只会进行计数器+1的操作,没有其他的操作了。由于Java中的锁是可重入锁,所以上述代码在Java中不会出现死锁的情况。

二、两个线程,两把锁

1.问题介绍

        当你创建了两个线程,线程一获取到了锁A,线程二获取到了锁B,接下来线程一尝试获取锁B,线程2尝试获取锁A,这时就会出现“死锁”,一旦出现“死锁”,线程就会“卡住了”,就无法再继续工作,这属于一个严重的bug,下面利用代码对这种情况进行演示:

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

        Thread t2 = new Thread(()->{
            synchronized (B) {
                // sleep 一下, 是给 t1 时间,让 t1 获取到 A
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                // 尝试获取到 A, 并没有释放 B
                synchronized (A) {
                    System.out.println("线程 2 拿到了两把锁");
                }
            }
        });

        t1.start();
        t2.start();
    }
}

        运行结果如下:         此时,线程一等线程二释放锁B,线程二等线程一释放锁A,两个线程互不想让,就出现了“死锁”这种状况,这就好比,你把家的钥匙锁在了车里,然而车的钥匙却锁在了家里。

2.解决方式

        面对上述的问题,最好的解决方式就是规定加锁的顺序,我们约定先对A加锁,然后在对B加锁,此时问题就会得到解决,按照这个方案对代码进行修改,代码如下:

public class Test2 {
    public static void main(String[] args) {
        Object A = new Object();
        Object B = new Object();
        Thread t1 = new Thread(()->{
            synchronized (A) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                // 尝试获取 B, 但没有释放 A
                synchronized (B) {
                    System.out.println("线程 1 拿到了两把锁");
                }
            }
        });

        Thread t2 = new Thread(()->{
            synchronized (A) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                // 尝试获取到 B, 并没有释放 A
                synchronized (B) {
                    System.out.println("线程 2 拿到了两把锁");
                }
            }
        });

        t1.start();
        t2.start();
    }
}

        运行结果: 

三、N个线程,M把锁

1.哲学家就餐问题

        N个线程,M把锁这种情况有一个非常经典的问题,这就是哲学家就餐问题,下面来简单介绍一下这个问题,如下图所示:

        话说从前有五个哲学家,他们坐在桌子前,在他们两两之间放着一根筷子,这里规定每个哲学家只能拿起双手边的两根筷子, 此时当一个哲学家要吃锅包肉的时候,坐在他旁边的两个哲学家就需要阻塞等待,只有当这个哲学家吃完的时候,主动放下筷子,他旁边的两个哲学家才能拿到他手里的筷子,这里虽然筷子的数量不充裕,但是也还好,因为每个哲学家除了吃锅包肉以外还要做一件事,就是“思考人生”,这时哲学家就会放下筷子,由于每个哲学家什么时候吃锅包肉,什么时候“思考人生”是不确定的,所以这个模型在一个特殊情况下是不可以正常工作的。

        假设,在同一时刻,所有的哲学家都想吃锅包肉了,他们同时抄起了左手的筷子,这个时候他们再想拿右手的筷子,就拿不到了,因为右手的筷子被别的哲学家给拿了,此时,由于所有的哲学家都不想放下已经拿起来的筷子,就要等旁边的哲学家放下筷子,可是没有哲学家吃到锅包肉,也就没有哲学家放下筷子,这样就出现了“死锁”,如下图所示:

        在上述问题中,每个哲学家就相当于一个线程,五个筷子就相当于五把锁,哲学家们啥时候吃锅包肉,啥时候“思考人生”这属于“随机调度”,绝大部分情况下是可以正常工作,但是出现上图情况是就会出现“死锁”。

2.解决方式

        解决“死锁”的问题,方案有很多种,在解决上述问题前,先解释一下“死锁”产生的四个必要条件(全部具备才可以,缺一不可):

  1. 互斥使用:获取锁的过程是互斥的,一个线程拿到了这把锁,另一个线程也想获取这把锁就需要阻塞等待。
  2. 不可抢占:一个线程拿到了锁之后,只能主动解锁,不能让别的线程强行把锁抢走。
  3. 请求保持:一个线程拿到了锁A之后,在持有锁A的前提下尝试获取锁B。
  4. 循环等待/环路等待:这是一种代码结构。

        解决死锁问题,核心思路,破坏上述的必要条件,破坏一个就可以解决, 那么这个问题可以从哪里入手呢?首先,第一个互斥使用和第二个不可抢占,这是锁的基本特性,不好破坏,其次,第三个请求保持,这个要看实际代码与实际需求,最后,第四个循环等待,关于代码结构,破坏起来是最容易的,所以我们解决这个问题从第四个必要条件开始。

        这里我们只有指定一定的规则,就可以有效的避免循环等待,比如,我们指定加锁的顺序:

        针对五把锁,都进行编号,约定每个线程获取锁的时候,一定要先获取编号小的锁,再获取编号大的锁。

        这种解决方案,可以如下图所示(为了表示方便,这里对哲学家们也进行了编号): 

        图中由于一号哲学家获取了一号筷子,导致二号哲学家想获取编号小的筷子时只能等一号哲学家放下筷子,由于规定必须先获取编号小的筷子后获取编号大的筷子,所以二号哲学家不会获取三号筷子,这样就保证在餐桌上至少会有一位哲学家正在进餐,上述图中表示的只是一种情况,规定完拿筷子的顺序之后就解决了哲学家进餐的问题。

        解决哲学家就餐问题其实有很多种方案:

  1. 多添置一些额外的筷子
  2. 减少一个哲学家
  3. 引入计数器,限制最多多少人吃锅包肉
  4. 引入拿筷子的规则
  5. 利用“银行家算法”

        这些方案中,1~3方案虽然不复杂,但是普适性不高,在一些特定需求中不可以使用,第5个方案,确实可以解决“死锁”问题,但是个人并不推荐,因为比较复杂,容易引入新的问题。 

·结尾

        本篇文章到这就快要结束了,在进行多线程编程时,线程安全是一个让我们感到复杂的问题,“死锁”只是其中问题之一,上述的所有内容都是在对“死锁”的问题进行介绍与解决,如果文章有问题欢迎在评论区进行讨论,如果感觉文章讲述还不错的话,也希望大家能给个三连支持一下~~你们的支持就是我最大的动力,我们下一篇文章再见吧┏(^0^)┛

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值