什么叫死锁?死锁案例?死锁必须满足哪些条件?如何定位死锁问题?有哪些解决死锁策略?哲学家问题?

1.死锁是什么?

死锁一定发生在并发环境中,死锁是一种状态,当两个(或者多个线程)相互持有对方所需要的资源,却又都不主动释放手中持有的资源,导致大家都获取不到自己想要的资源,所有相关的线程无法继续执行。

2.死锁案例

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * @author weijie
 * @date 2020/4/28 10:43
 */
public class DeadLockDemo {

    Object o1 = new Object();
    Object o2 = new Object();
    class Task1 implements Runnable{

        @Override
        public void run() {
            synchronized (o1){
                System.out.println("task1 start ...");
                String threadName = Thread.currentThread().getName();
                System.out.print(threadName + "获取i1对象锁--->");
                /**
                 * 阻塞当前线程
                 */
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (o2){
                    System.out.println("获取i2对象锁");
                }
                System.out.println("task2 end ...");
            }
        }
    }

    class Task2 implements Runnable{

        @Override
        public void run() {
            synchronized (o2) {
                System.out.println("task2 start ...");
                String threadName = Thread.currentThread().getName();
                System.out.print(threadName + "获取i2对象锁--->");
                synchronized (o1) {
                    System.out.println("获取i1对象锁");
                }
                System.out.println("task 2 end ...");
            }
        }
    }


    public static void main(String[] args) {
        DeadLockDemo deadLockDemo = new DeadLockDemo();
        ExecutorService executorService = Executors.newFixedThreadPool(2);
        executorService.submit(deadLockDemo.new Task1());
        executorService.submit(deadLockDemo.new Task2());

    }

}


3.死锁必须满足的四个条件?

  1. 互斥条件:每个资源同时只能被一个线程使用。线程A占用了,线程B一定不能使用,必须等线程A释放资源后,线程B才能继续使用。
  2. 请求与保持条件:也就是说当线程A占用资源时,由于线程A有其他业务逻辑导致线程阻塞等,此时不能自动释放资源,如果自动释放资源那就不会发生死锁问题。
  3. 不剥夺条件:当线程A占用资源时,不能被线程B剥夺、抢占资源。

4.如何用命令行和代码定位死锁?

1.通过命令行定位

  1. 通过jps指令获取服务进程id
56402 MustDeadLock
56403 Launcher
56474 Jps
55051 KotlinCompileDaemon

  1. jstack pid指令来打印信息,会出现“Found one Java-level deadlock”表示死锁,接着我们可以通过打印信息定位死锁问题
Found one Java-level deadlock:
=============================
"t2":
  waiting to lock monitor 0x00007fa06c004a18 (object 0x000000076adabaf0, a java.lang.Object),
  which is held by "t1"
"t1":
  waiting to lock monitor 0x00007fa06c007358 (object 0x000000076adabb00, a java.lang.Object),
  which is held by "t2"

Java stack information for the threads listed above:
===================================================
"t2":
	at lesson67.MustDeadLock.run(MustDeadLock.java:31)
	- waiting to lock <0x000000076adabaf0> (a java.lang.Object)
	- locked <0x000000076adabb00> (a java.lang.Object)
	at java.lang.Thread.run(Thread.java:748)
"t1":
	at lesson67.MustDeadLock.run(MustDeadLock.java:19)
	- waiting to lock <0x000000076adabb00> (a java.lang.Object)
	- locked <0x000000076adabaf0> (a java.lang.Object)
	at java.lang.Thread.run(Thread.java:748)

Found 1 deadlock

在这里它首先会打印“Found one Java-level deadlock”,表明“找到了一个死锁”。
然后是更详细的信息,从中间这部分的信息中可以看出:
t2 线程想要去获取这个尾号为 af0 的锁对象,但是它被 t1 线程持有,同时 t2 持有尾号为 b00 的锁对象; 相反,t1 想要获取尾号为 b00 的锁对象,但是它被 t2 线程持有,同时 t1 持有的却是尾号为 af0 的锁对象, 这就形成了一个依赖环路,发生了死锁。 最后它还打印出了“Found 1 deadlock.”

可以看出,jstack 工具不但帮我们找到了死锁,甚至还把哪个线程、想要获取哪个锁、形成什么样的环路都告诉我们了,当我们有了这样的信息之后,死锁就非常容易定位了,所以接下来我们就可以进一步修改代码,来避免死锁了。

2.代码定位死锁

ThreadMXBean用来定位死锁问题

在main方法中添加如下代码:

        //保证发生死锁
        Thread.sleep(3000);
        System.out.println("定位死锁信息");
        ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
        long[] deadlockedThreads = threadMXBean.findDeadlockedThreads();
        if(deadlockedThreads != null && deadlockedThreads.length > 0){
            for (int i= 0; i < deadlockedThreads.length; i++){
                long deadlockedThread = deadlockedThreads[i];
                ThreadInfo threadInfo = threadMXBean.getThreadInfo(deadlockedThread);
                System.out.println("线程id为"+threadInfo.getThreadId()+",线程名为" + threadInfo.getThreadName()+"的线程已经发生死锁,需要的锁正被线程"+threadInfo.getLockOwnerName()+"持有。");
            }
        }

在这里插入图片描述
可以看出,ThreadMXBean 也可以帮我们找到并定位死锁,如果我们在业务代码中加入这样的检测,那我们就可以在发生死锁的时候及时地定位,同时进行报警等其他处理,也就增强了我们程序的健壮性

5.经典的哲学家问题

1.问题描述

哲学家就餐问题也被称为刀叉问题,或者吃面问题。我们先来描述一下这个问题所要说明的事情,这个问题如下图所示:

在这里插入图片描述

有 5 个哲学家,他们面前都有一双筷子,即左手有一根筷子,右手有一根筷子。当然,这个问题有多个版本的描述,可以说是筷子,也可以说是一刀一叉,因为吃牛排的时候,需要刀和叉,缺一不可,也有说是用两把叉子来吃意大利面。这里具体是刀叉还是筷子并不重要,重要的是必须要同时持有左右两边的两个才行,也就是说,哲学家左手要拿到一根筷子,右手也要拿到一根筷子,在这种情况下哲学家才能吃饭。为了方便理解,我们选取和我国传统最贴近的筷子来说明这个问题。

为什么选择哲学家呢?因为哲学家的特点是喜欢思考,所以我们可以把哲学家一天的行为抽象为思考,然后吃饭,并且他们吃饭的时候要用一双筷子,而不能只用一根筷子。

1. 主流程

我们来看一下哲学家就餐的主流程。哲学家如果想吃饭,他会先尝试拿起左手的筷子,然后再尝试拿起右手的筷子,如果某一根筷子被别人使用了,他就得等待他人用完,用完之后他人自然会把筷子放回原位,接着他把筷子拿起来就可以吃了(不考虑卫生问题)。这就是哲学家就餐的最主要流程。

2. 流程的伪代码

我们来看一下这个流程的伪代码,如下所示:

while(true) { 
    // 思考人生、宇宙、万物...
    think();
 
    // 思考后感到饿了,需要拿筷子开始吃饭
    pick_up_left_chopstick();
    pick_up_right_chopstick();
    eat();
    put_down_right_chopstick();
    put_down_left_chopstick();
 
    // 吃完饭后,继续思考人生、宇宙、万物...
}

while(true) 代表整个是一个无限循环。在每个循环中,哲学家首先会开始思考,思考一段时间之后(这个时间长度可以是随机的),他感到饿了,就准备开始吃饭。在吃饭之前必须先拿到左手的筷子,再拿到右手的筷子,然后才开始吃饭;吃完之后,先放回右手的筷子,再放回左手的筷子;由于这是个 while 循环,所以他就会继续思考人生,开启下一个循环。这就是整个过程。

3.有死锁和资源耗尽的风险

这里存在什么风险呢?就是发生死锁的风险。如下面的动画所示:
在这里插入图片描述
根据我们的逻辑规定,在拿起左手边的筷子之后,下一步是去拿右手的筷子。大部分情况下,右边的哲学家正在思考,所以当前哲学家的右手边的筷子是空闲的,或者如果右边的哲学家正在吃饭,那么当前的哲学家就等右边的哲学家吃完饭并释放筷子,于是当前哲学家就能拿到了他右手边的筷子了。

但是,如果每个哲学家都同时拿起左手的筷子,那么就形成了环形依赖,在这种特殊的情况下,每个人都拿着左手的筷子,都缺少右手的筷子,那么就没有人可以开始吃饭了,自然也就没有人会放下手中的筷子。这就陷入了死锁,形成了一个相互等待的情况。

4.代码演示

代码如下所示:

package com.netty.rpc.test.util;

import java.lang.management.ManagementFactory;
import java.lang.management.ThreadInfo;
import java.lang.management.ThreadMXBean;

public class DiningPhilosophers {

    public static class Philosopher implements Runnable {

        private Object leftChopstick;
        private Object rightChopstick;

        public Philosopher(Object leftChopstick, Object rightChopstick) {
            this.leftChopstick = leftChopstick;
            this.rightChopstick = rightChopstick;
        }

        @Override
        public void run() {
            try {
                while (true) {
                    doAction("思考人生、宇宙、万物、灵魂...");
                    synchronized (leftChopstick) {
                        doAction("拿起左边的筷子");
                        synchronized (rightChopstick) {
                            doAction("拿起右边的筷子");
                            doAction("吃饭");
                            doAction("放下右边的筷子");
                        }
                        doAction("放下左边的筷子");
                    }
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        private void doAction(String action) throws InterruptedException {
            System.out.println(Thread.currentThread().getName() + " " + action);
            Thread.sleep((long) (Math.random() * 10));
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Philosopher[] philosophers = new Philosopher[5];
        Object[] chopsticks = new Object[philosophers.length];
        for (int i = 0; i < chopsticks.length; i++) {
            chopsticks[i] = new Object();
        }
        for (int i = 0; i < philosophers.length; i++) {
            Object leftChopstick = chopsticks[i];
            Object rightChopstick = chopsticks[(i + 1) % chopsticks.length];
            philosophers[i] = new Philosopher(rightChopstick, leftChopstick);
            new Thread(philosophers[i], "哲学家" + (i + 1) + "号").start();
        }

        Thread.sleep(1000 * 10);
        ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
        long[] deadlockedThreads = threadMXBean.findDeadlockedThreads();
        if(deadlockedThreads != null && deadlockedThreads.length > 0){
            for (int i= 0; i < deadlockedThreads.length; i++){
                long deadlockedThread = deadlockedThreads[i];
                ThreadInfo threadInfo = threadMXBean.getThreadInfo(deadlockedThread);
                System.out.println("线程id为"+threadInfo.getThreadId()+",线程名为" + threadInfo.getThreadName()+"的线程已经发生死锁,需要的锁正被线程"+threadInfo.getLockOwnerName()+"持有。");
            }
        }
    }
}

运行后截图:
在这里插入图片描述
哲学家 1、3、2、4、5 几乎同时开始思考,然后,假设他们思考的时间比较相近,于是他们都在几乎同一时刻想开始吃饭,都纷纷拿起左手的筷子,这时就陷入了死锁状态,没有人可以拿到右手的筷子,也就没有人可以吃饭,于是陷入了无穷等待,这就是经典的哲学家就餐问题。

5.多种解决方案

对于这个问题我们该如何解决呢?有多种解决方案,这里我们讲讲其中的几种。前面我们讲过,要想解决死锁问题,只要破坏死锁四个必要条件的任何一个都可以。

1. 服务员检查

第一个解决方案就是引入服务员检查机制。比如我们引入一个服务员,当每次哲学家要吃饭时,他需要先询问服务员:我现在能否去拿筷子吃饭?此时,服务员先判断他拿筷子有没有发生死锁的可能,假如有的话,服务员会说:现在不允许你吃饭。这是一种解决方案。

2. 领导调节

我们根据上一讲的死锁检测和恢复策略,可以引入一个领导,这个领导进行定期巡视。如果他发现已经发生死锁了,就会剥夺某一个哲学家的筷子,让他放下。这样一来,由于这个人的牺牲,其他的哲学家就都可以吃饭了。这也是一种解决方案。

3. 改变一个哲学家拿筷子的顺序

我们还可以利用死锁避免策略,那就是从逻辑上去避免死锁的发生,比如改变其中一个哲学家拿筷子的顺序。我们可以让 4 个哲学家都先拿左边的筷子再拿右边的筷子,但是有一名哲学家与他们相反,他是先拿右边的再拿左边的,这样一来就不会出现循环等待同一边筷子的情况,也就不会发生死锁了。

死锁解决
我们把“改变一个哲学家拿筷子的顺序”这件事情用代码来写一下,修改后的 main 方法如下:

        Philosopher[] philosophers = new Philosopher[5];
        Object[] chopsticks = new Object[philosophers.length];
        for (int i = 0; i < chopsticks.length; i++) {
            chopsticks[i] = new Object();
        }
        for (int i = 0; i < philosophers.length; i++) {
            Object leftChopstick = chopsticks[i];
            Object rightChopstick = chopsticks[(i + 1) % chopsticks.length];
            if (i == philosophers.length - 1){
                philosophers[i] = new Philosopher(rightChopstick, leftChopstick);
            }else{
                philosophers[i] = new Philosopher(leftChopstick, rightChopstick);
            }
//            philosophers[i] = new Philosopher(rightChopstick, leftChopstick);
            new Thread(philosophers[i], "哲学家" + (i + 1) + "号").start();
        }

if (i == philosophers.length - 1) ,在这种情况下,我们给它传入的筷子顺序恰好相反,这样一来,他拿筷子的顺序也就相反了,他会先拿起右边的筷子,再拿起左边的筷子。那么这个程序运行的结果,是所有哲学家都可以正常地去进行思考和就餐了,并且不会发生死锁。

  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值