并发编程之死锁问题介绍

一、本文概览

死锁问题在并发编程中是一个非常致命的问题,问题一旦产生,只能通过重启机器、修改代码来修复问题,下面我们通过一小段文章内容介绍下死锁以及如何死锁的预防

在这里插入图片描述

二、什么是死锁?

在介绍死锁之前,先来明确下什么是锁?

  • 锁其实就是一种同步机制,一个线程拥有对一块资源的锁,那么该线程对这块资源的处理是与其他线程互斥的!在该线程未释放锁之前,其它线程会被限制对统一资源的访问。

有了对锁概念的认知,我们再来看下死锁是什么?所谓死锁其实就是指在多线程或者多进程运行状态下,因争夺资源而导致的一种互不让步的僵局,导致各自均进入阻塞状态,如果没有外力(强制中断或者程序设置超时中断)的推动,则程序将一直处于僵持(崩溃)状态。

三、死锁产生的必要条件?

产生死锁有四个必要条件:

  • 资源互斥:资源互斥就是上面我们说的锁,线程持有的锁是需要具备独占且排他使用的
  • 不可被剥夺:线程在对持有的资源未使用完毕前,是不会被其他线程强行剥夺的
  • 请求并保持:线程在请求获取新的资源时,当前所持有的资源依旧继续占用
  • 循环等待:多个线程对于获取锁行为是一个环形,例如线程A持有锁1,需要获取锁2才能进行后续操作;线程B持有锁2,需要获取锁1才能进行后续操作,此时就形成了环

我们来看看死锁的代码示例:

package com.markus.onjava.concurrent;

/**
 * @author: markus
 * @date: 2023/2/22 10:21 PM
 * @Description: 死锁代码演示
 * @Blog: https://markuszhang.com
 * It's my honor to share what I've learned with you!
 */
public class DeadLockDemo {
    public static void main(String[] args) {
        String lockA = "lockA";
        String lockB = "lockB";
        Thread thread1 = new Thread(new Task(lockA, lockB));
        Thread thread2 = new Thread(new Task(lockB, lockA));
        thread1.start();
        thread2.start();
    }
}

class Task implements Runnable {

    private final String lockA;
    private final String lockB;

    public Task(String lockA, String lockB) {
        this.lockA = lockA;
        this.lockB = lockB;
    }

    @Override
    public void run() {
        try {
            synchronized (lockA) {
                log("contain lock:[" + lockA + "]");
                Thread.sleep(2000);
                synchronized (lockB) {
                    log("contain lock:[" + lockB + "]");
                }
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    private void log(String msg) {
        System.out.println("Thread id [" + Thread.currentThread().getId() + "] msg: " + msg);
    }
}

控制台

在这里插入图片描述

我们可以通过jstack来查看进程中的线程状态来分析是哪些线程出现了死锁状态

# 查询进程id
➜  ~ jps
52996 Launcher
53734 Jps
53498 Launcher
53499 DeadLockDemo
5452
52862 RemoteMavenServer36
# 分析进程的堆栈信息
➜  ~ jstack 53499
2023-02-22 22:58:31
Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.211-b12 mixed mode):

"Thread-1" #12 prio=5 os_prio=31 tid=0x00007f9e2d84d000 nid=0x5b03 waiting for monitor entry [0x000000030a74d000]
   java.lang.Thread.State: BLOCKED (on object monitor)
	at com.markus.onjava.concurrent.Task.run(DeadLockDemo.java:38)
	- waiting to lock <0x000000076ac1c8d0> (a java.lang.String)
	- locked <0x000000076ac1c908> (a java.lang.String)
	at java.lang.Thread.run(Thread.java:748)

"Thread-0" #11 prio=5 os_prio=31 tid=0x00007f9e2d84c800 nid=0x9c5f waiting for monitor entry [0x000000030a64a000]
   java.lang.Thread.State: BLOCKED (on object monitor)
	at com.markus.onjava.concurrent.Task.run(DeadLockDemo.java:38)
	- waiting to lock <0x000000076ac1c908> (a java.lang.String)
	- locked <0x000000076ac1c8d0> (a java.lang.String)
	at java.lang.Thread.run(Thread.java:748)

# ... 省略部分线程堆栈信息
Found one Java-level deadlock:
=============================
"Thread-1":
  waiting to lock monitor 0x00007f9e2f814168 (object 0x000000076ac1c8d0, a java.lang.String),
  which is held by "Thread-0"
"Thread-0":
  waiting to lock monitor 0x00007f9e2f8169f8 (object 0x000000076ac1c908, a java.lang.String),
  which is held by "Thread-1"

Java stack information for the threads listed above:
===================================================
"Thread-1":
	at com.markus.onjava.concurrent.Task.run(DeadLockDemo.java:38)
	- waiting to lock <0x000000076ac1c8d0> (a java.lang.String)
	- locked <0x000000076ac1c908> (a java.lang.String)
	at java.lang.Thread.run(Thread.java:748)
"Thread-0":
	at com.markus.onjava.concurrent.Task.run(DeadLockDemo.java:38)
	- waiting to lock <0x000000076ac1c908> (a java.lang.String)
	- locked <0x000000076ac1c8d0> (a java.lang.String)
	at java.lang.Thread.run(Thread.java:748)

Found 1 deadlock.

四、如何防范死锁?

上面介绍了产生死锁的必要条件,所谓必要条件就是要都满足才能产生死锁,所以在预防的时候,我们可以任意打破死锁的4个必要条件来预防死锁,但又因为资源互斥是底层操作系统的固有特性,应用层面是无法改变的,所以我们可以通过破坏剩余的三个条件来进行预防:

  • 破坏"不可被剥夺"条件
  • 破坏"请求并保持"条件
  • 破坏"循环等待"条件

下面来举个经典的死锁案例:哲学家晚餐情景。

一个餐桌上有五名哲学家以及五根筷子,这五根筷子分别插入到五位哲学家的间隔处,哲学家想要饮食则必须同时拿起左右手两边的筷子,所以现在就有问题了,如果有人能够吃饭,那肯定会有人等待,在某种情况下还极可能造成所有人都在等待以导致死锁的情况。

在这里插入图片描述

我们来用代码实现一下这种场景:

  • 筷子持有
package com.markus.onjava.concurrent.deadlock;

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;

/**
 * @author: markus
 * @date: 2023/2/25 10:12 PM
 * @Description: 筷子持有者
 * @Blog: https://markuszhang.com
 * It's my honor to share what I've learned with you!
 */
public class StickHolder {
    private static class Chopstick {
    }

    private Chopstick stick = new Chopstick();

    // 利用阻塞队列实现当前筷子同一时刻只能被一个人持有,其他人想要获取这跟筷子,则必须等待
    private BlockingQueue<Chopstick> holder = new ArrayBlockingQueue<>(1);

    public StickHolder() {
        putDown();
    }

    /**
     * 拿起筷子
     */
    public void pickUp() {
        try {
            holder.take(); // 不可用时会阻塞
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * 放下筷子
     */
    public void putDown() {
        try {
            holder.put(stick);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }
}
  • 哲学家
package com.markus.onjava.concurrent.deadlock;

import java.util.concurrent.TimeUnit;

/**
 * @author: markus
 * @date: 2023/2/25 10:19 PM
 * @Description: 哲学家
 * @Blog: https://markuszhang.com
 * It's my honor to share what I've learned with you!
 */
public class Philosopher implements Runnable {
    private final int seat;
    private final StickHolder left, right;

    public Philosopher(int seat, StickHolder left, StickHolder right) {
        this.seat = seat;
        this.left = left;
        this.right = right;
    }

    @Override
    public String toString() {
        return "P" + seat;
    }

    @Override
    public void run() {
      	// 循环执行吃饭的动作
        while (true) {
            // 拿起右手边的筷子
            right.pickUp();
            // 拿起左手边的筷子
            left.pickUp();
            System.out.println(this + " eating");
            // 放下右手边的筷子
            right.putDown();
            // 放下左手边的筷子
            left.putDown();
        }
    }
}

  • 晚餐现场
package com.markus.onjava.concurrent.deadlock;

import com.markus.onjava.Nap;

import java.util.Arrays;
import java.util.concurrent.CompletableFuture;

/**
 * @author: markus
 * @date: 2023/2/25 10:23 PM
 * @Description: 哲学家的晚餐演示
 * @Blog: https://markuszhang.com
 * It's my honor to share what I've learned with you!
 */
public class DiningPhilosophers {
    /*筷子持有者*/
    private StickHolder[] sticks;
    /*哲学家*/
    private Philosopher[] philosophers;

    public DiningPhilosophers(int n) {
        sticks = new StickHolder[n];
        Arrays.setAll(sticks, i -> new StickHolder());
        philosophers = new Philosopher[n];
	      // 通过模数n选择右手边的筷子,并将最后一个哲学家指向第一位哲学家旁边,整体形成一个环
        Arrays.setAll(philosophers, i -> new Philosopher(i, sticks[i], sticks[(i + 1) % n])); 
        Arrays.stream(philosophers)
                .forEach(CompletableFuture::runAsync);

    }

    public static void main(String[] args) {
      	// 立刻返回,不阻塞
        new DiningPhilosophers(5);
      	// 主流程等待100s后再退出(此时我们可以观察哲学家的用餐场景)
        new Nap(100, "Shutdown");
    }
}

执行晚餐现场的代码后,我们可以发现,起初还能正常的执行,但一会就会形成互相等待的场景,这种等待形成了一个环,也就造成了死锁。

在这里插入图片描述

我们也可以通过jstack命令来确认下互相等待的情况:

➜  ~ jps
1593
10331 Jps
10318 Launcher
8862 JStack
10319 DiningPhilosophers
➜  ~ jstack 10319
2023-02-26 14:51:08
Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.211-b12 mixed mode):

"ForkJoinPool.commonPool-worker-5" #17 daemon prio=5 os_prio=31 tid=0x00007fb25188f800 nid=0x6a07 waiting on condition [0x000000030668e000]
   java.lang.Thread.State: WAITING (parking)
	at sun.misc.Unsafe.park(Native Method)
	- parking to wait for  <0x000000076b249450> (a java.util.concurrent.ForkJoinPool)
	at java.util.concurrent.ForkJoinPool.awaitWork(ForkJoinPool.java:1824)
	at java.util.concurrent.ForkJoinPool.runWorker(ForkJoinPool.java:1693)
	at java.util.concurrent.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:157)

"ForkJoinPool.commonPool-worker-7" #19 daemon prio=5 os_prio=31 tid=0x00007fb211809000 nid=0x9503 waiting on condition [0x000000030658b000]
   java.lang.Thread.State: WAITING (parking)
	at sun.misc.Unsafe.park(Native Method)
	- parking to wait for  <0x000000076b249450> (a java.util.concurrent.ForkJoinPool)
	at java.util.concurrent.ForkJoinPool.awaitWork(ForkJoinPool.java:1824)
	at java.util.concurrent.ForkJoinPool.runWorker(ForkJoinPool.java:1693)
	at java.util.concurrent.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:157)

"ForkJoinPool.commonPool-worker-6" #18 daemon prio=5 os_prio=31 tid=0x00007fb211808800 nid=0x6703 waiting on condition [0x0000000306488000]
   java.lang.Thread.State: WAITING (parking)
	at sun.misc.Unsafe.park(Native Method)
	- parking to wait for  <0x000000076b18a040> (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject)
	at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)
	at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:2039)
	at java.util.concurrent.ArrayBlockingQueue.take(ArrayBlockingQueue.java:403)
	at com.markus.onjava.concurrent.deadlock.StickHolder.pickUp(StickHolder.java:30)
	at com.markus.onjava.concurrent.deadlock.Philosopher.run(Philosopher.java:31)
	at java.util.concurrent.CompletableFuture$AsyncRun.run$$$capture(CompletableFuture.java:1626)
	at java.util.concurrent.CompletableFuture$AsyncRun.run(CompletableFuture.java)
	at java.util.concurrent.CompletableFuture$AsyncRun.exec(CompletableFuture.java:1618)
	at java.util.concurrent.ForkJoinTask.doExec$$$capture(ForkJoinTask.java:289)
	at java.util.concurrent.ForkJoinTask.doExec(ForkJoinTask.java)
	at java.util.concurrent.ForkJoinPool$WorkQueue.runTask(ForkJoinPool.java:1056)
	at java.util.concurrent.ForkJoinPool.runWorker(ForkJoinPool.java:1692)
	at java.util.concurrent.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:157)

"ForkJoinPool.commonPool-worker-4" #16 daemon prio=5 os_prio=31 tid=0x00007fb2540cd000 nid=0x9703 waiting on condition [0x0000000306385000]
   java.lang.Thread.State: WAITING (parking)
	at sun.misc.Unsafe.park(Native Method)
	- parking to wait for  <0x000000076b189f70> (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject)
	at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)
	at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:2039)
	at java.util.concurrent.ArrayBlockingQueue.take(ArrayBlockingQueue.java:403)
	at com.markus.onjava.concurrent.deadlock.StickHolder.pickUp(StickHolder.java:30)
	at com.markus.onjava.concurrent.deadlock.Philosopher.run(Philosopher.java:31)
	at java.util.concurrent.CompletableFuture$AsyncRun.run$$$capture(CompletableFuture.java:1626)
	at java.util.concurrent.CompletableFuture$AsyncRun.run(CompletableFuture.java)
	at java.util.concurrent.CompletableFuture$AsyncRun.exec(CompletableFuture.java:1618)
	at java.util.concurrent.ForkJoinTask.doExec$$$capture(ForkJoinTask.java:289)
	at java.util.concurrent.ForkJoinTask.doExec(ForkJoinTask.java)
	at java.util.concurrent.ForkJoinPool$WorkQueue.runTask(ForkJoinPool.java:1056)
	at java.util.concurrent.ForkJoinPool.runWorker(ForkJoinPool.java:1692)
	at java.util.concurrent.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:157)

"ForkJoinPool.commonPool-worker-3" #15 daemon prio=5 os_prio=31 tid=0x00007fb253849000 nid=0x9903 waiting on condition [0x0000000306282000]
   java.lang.Thread.State: WAITING (parking)
	at sun.misc.Unsafe.park(Native Method)
	- parking to wait for  <0x000000076b189ea0> (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject)
	at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)
	at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:2039)
	at java.util.concurrent.ArrayBlockingQueue.take(ArrayBlockingQueue.java:403)
	at com.markus.onjava.concurrent.deadlock.StickHolder.pickUp(StickHolder.java:30)
	at com.markus.onjava.concurrent.deadlock.Philosopher.run(Philosopher.java:31)
	at java.util.concurrent.CompletableFuture$AsyncRun.run$$$capture(CompletableFuture.java:1626)
	at java.util.concurrent.CompletableFuture$AsyncRun.run(CompletableFuture.java)
	at java.util.concurrent.CompletableFuture$AsyncRun.exec(CompletableFuture.java:1618)
	at java.util.concurrent.ForkJoinTask.doExec$$$capture(ForkJoinTask.java:289)
	at java.util.concurrent.ForkJoinTask.doExec(ForkJoinTask.java)
	at java.util.concurrent.ForkJoinPool$WorkQueue.runTask(ForkJoinPool.java:1056)
	at java.util.concurrent.ForkJoinPool.runWorker(ForkJoinPool.java:1692)
	at java.util.concurrent.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:157)

"ForkJoinPool.commonPool-worker-2" #14 daemon prio=5 os_prio=31 tid=0x00007fb252030800 nid=0x6303 waiting on condition [0x000000030617f000]
   java.lang.Thread.State: WAITING (parking)
	at sun.misc.Unsafe.park(Native Method)
	- parking to wait for  <0x000000076b189dd0> (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject)
	at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)
	at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:2039)
	at java.util.concurrent.ArrayBlockingQueue.take(ArrayBlockingQueue.java:403)
	at com.markus.onjava.concurrent.deadlock.StickHolder.pickUp(StickHolder.java:30)
	at com.markus.onjava.concurrent.deadlock.Philosopher.run(Philosopher.java:31)
	at java.util.concurrent.CompletableFuture$AsyncRun.run$$$capture(CompletableFuture.java:1626)
	at java.util.concurrent.CompletableFuture$AsyncRun.run(CompletableFuture.java)
	at java.util.concurrent.CompletableFuture$AsyncRun.exec(CompletableFuture.java:1618)
	at java.util.concurrent.ForkJoinTask.doExec$$$capture(ForkJoinTask.java:289)
	at java.util.concurrent.ForkJoinTask.doExec(ForkJoinTask.java)
	at java.util.concurrent.ForkJoinPool$WorkQueue.runTask(ForkJoinPool.java:1056)
	at java.util.concurrent.ForkJoinPool.runWorker(ForkJoinPool.java:1692)
	at java.util.concurrent.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:157)

"ForkJoinPool.commonPool-worker-1" #13 daemon prio=5 os_prio=31 tid=0x00007fb25383a000 nid=0x6133 waiting on condition [0x000000030607c000]
   java.lang.Thread.State: WAITING (parking)
	at sun.misc.Unsafe.park(Native Method)
	- parking to wait for  <0x000000076b189d00> (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject)
	at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)
	at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:2039)
	at java.util.concurrent.ArrayBlockingQueue.take(ArrayBlockingQueue.java:403)
	at com.markus.onjava.concurrent.deadlock.StickHolder.pickUp(StickHolder.java:30)
	at com.markus.onjava.concurrent.deadlock.Philosopher.run(Philosopher.java:31)
	at java.util.concurrent.CompletableFuture$AsyncRun.run$$$capture(CompletableFuture.java:1626)
	at java.util.concurrent.CompletableFuture$AsyncRun.run(CompletableFuture.java)
	at java.util.concurrent.CompletableFuture$AsyncRun.exec(CompletableFuture.java:1618)
	at java.util.concurrent.ForkJoinTask.doExec$$$capture(ForkJoinTask.java:289)
	at java.util.concurrent.ForkJoinTask.doExec(ForkJoinTask.java)
	at java.util.concurrent.ForkJoinPool$WorkQueue.runTask(ForkJoinPool.java:1056)
	at java.util.concurrent.ForkJoinPool.runWorker(ForkJoinPool.java:1692)
	at java.util.concurrent.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:157)


通过对上面基础知识的学习,我们可以知道代码产生死锁的原因有以下几点:

  • 一根筷子(资源)同一时刻只能被一位哲学家使用,也就是资源互斥
  • 哲学家每人都持有一根筷子,并且均尝试去获取另一根筷子,就是请求并保持
  • 哲学家持有的筷子不能被强制回收,只能由自己完成任务主动释放,也就是不可被剥夺
  • 哲学家的等待形成了一个环,造成循环等待

前面我们说了资源互斥是系统特性,我们先忽略打破这个条件,下面我们来通过其他三个条件来实现预防死锁的解决方案:

  • 打破循环等待-我们可以看到,哲学家都是尝试先拿起右边的筷子再拿起右边的筷子,这样就互相之间就形成了环,我们可以指定某一位哲学家不按照这样的顺序取筷子,则形成不了环,也就不会造成情况了。
package com.markus.onjava.concurrent.deadlock;

import com.markus.onjava.Nap;

import java.util.Arrays;
import java.util.concurrent.CompletableFuture;

/**
 * @author: markus
 * @date: 2023/2/25 10:23 PM
 * @Description: 哲学家的晚餐演示
 * @Blog: https://markuszhang.com
 * It's my honor to share what I've learned with you!
 */
public class DiningPhilosophers {
    /*筷子持有者*/
    private StickHolder[] sticks;
    /*哲学家*/
    private Philosopher[] philosophers;

    public DiningPhilosophers(int n) {
        sticks = new StickHolder[n];
        Arrays.setAll(sticks, i -> new StickHolder());
        philosophers = new Philosopher[n];
        Arrays.setAll(philosophers, i -> new Philosopher(i, sticks[i], sticks[(i + 1) % n]));
        philosophers[1] = new Philosopher(1, sticks[0], sticks[1]); // [1] 通过将第2位哲学家颠倒拿放筷子的顺序来修正死锁
        Arrays.stream(philosophers)
                .forEach(CompletableFuture::runAsync);

    }

    public static void main(String[] args) {
        new DiningPhilosophers(5);
        new Nap(100, "Shutdown");
    }
}
  • 打破循环条件还有另一种方案,就是调大资源数超出你的机器的CPU物理核数,这样就导致同一时刻,肯定会有一位哲学家不参与活动的进行,这也就形成不了循环等待的情况
  • 我们还可以通过打破请求并保持的条件
/**
 * 拿起筷子
 */
public void pickUp() {
    try {
//      holder.take(); // 不可用时会阻塞
        holder.poll(100, TimeUnit.MILLISECONDS);// 超时自己中断,不再尝试去获取锁
    } catch (InterruptedException e) {
//      throw new RuntimeException(e);
    }
}
  • 不可被剥夺条件实现起来比较麻烦,核心思想就是如果拿不到就去旁边手中抢过来。

综上,预防死锁的方式可以通过打破那四个必要条件的其中一个即可,其中打破循环等待条件又是最容易的,我们通常会通过这种思路进行预防。

五、本文总结

上述就是对死锁的简单介绍,包括死锁是什么,什么情况下会导致死锁(四个必要条件)以及如何预防死锁(打破四个必要条件之一即可),我们也可以看出在Java语言层面上是无法支持我们来避免死锁的,所以我们只能通过谨慎的设计来避免这个问题。避免并发问题,最简单有效的方法就是永远不要共享资源,但这看来也是不现实的,所以在设计的时候,我们也可以通过画布画出资源的依赖关系来谨慎设计方案等等其他方法来避免系统出现死锁。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值