并发编程(二十四)死锁

173 阅读4分钟

--- highlight: a11y-dark

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第22天,点击查看活动详情

现在网上死锁的内容太多了,很详细,内容很好,我这里没什么可补充和修改的。

1.死锁条件

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

互斥:一个资源每次只能被一个进程使用 (资源独立)。

请求与保持:一个进程因请求资源而阻塞时,对已获得的资源保持不放 (不释放锁)。

不剥夺:进程已获得的资源,在未使用之前,不能强行剥夺 (抢夺资源)。

循环等待:若干进程之间形成一种头尾相接的循环等待的资源关闭 (死循环)。

2.如何避免死锁

  1. 破坏” 互斥” 条件:系统里取消互斥、若资源一般不被一个进程独占使用,那么死锁是肯定不会发生的,但一般 “互斥” 条件是无法破坏的,因此,在死锁预防里主要是破坏其他三个必要条件,而不去涉及破坏 “互斥” 条件。

  2. 破坏 “请求和保持” 条件:

方法 1:所有的进程在开始运行之前,必须一次性的申请其在整个运行过程各种所需要的全部资源。 优点:简单易实施且安全。 缺点:因为某项资源不满足,进程无法启动,而其他已经满足了的资源也不会得到利用,严重降低了资源的利用率,造成资源浪费。

方法 2:该方法是对第一种方法的改进,允许进程只获得运行初期需要的资源,便开始运行,在运行过程中逐步释放掉分配到,已经使用完毕的资源,然后再去请求新的资源。这样的话资源的利用率会得到提高,也会减少进程的饥饿问题。

  1. 破坏 “不剥夺” 条件:当一个已经持有了一些资源的进程在提出新的资源请求没有得到满足时,它必须释放已经保持的所有资源,待以后需要使用的时候再重新申请。这就意味着进程已占有的资源会被短暂的释放或者说被抢占了。

  2. 破坏 “循环等待” 条件:可以通过定义资源类型的线性顺序来预防,可以将每个资源编号,当一个进程占有编号为 i 的资源时,那么它下一次申请资源只能申请编号大于 i 的资源。

3.常见避免死锁方法

现在我们介绍避免死锁的几个常见方法。

·避免一个线程同时获取多个锁。

·避免一个线程在锁内同时占用多个资源,尽量保证每个锁只占用一个资源。

·尝试使用定时锁,使用lock.tryLock(timeout)来替代使用内部锁机制。

·对于数据库锁,加锁和解锁必须在一个数据库连接里,否则会出现解锁失败的情况。

3.死锁加深记忆

我们先回忆一下我之前这篇文章《并发编程(三)java线程状态log日志解读 - Stack log 解读》中的例子,其中我我把死锁摘出来简单再说明一下,加强一下记忆。

示例代码如下:

public class DeadLock {
    private static String A = "A";
    private static String B = "B";
    public static void main(String[] args) {
        new DeadLock().deadLock();
    }
    private void deadLock() {
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (A) {
                    try {
                        Thread.currentThread().sleep(2000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    synchronized (B) {
                        System.out.println("1");
                    }
                }
            }
        });
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (B) {
                    synchronized (A) {
                        System.out.println("2");
                    }
                }
            }
        });
        t1.start();
        t2.start();
    }
}

执行效果如下:

image.png

先输入jps查看线程的pid,再根据就jstack pid查看到线程当前类所有栈信息的运行日志如下:

"Thread-1" #21 prio=5 os_prio=0 tid=0x00000000204a7800 nid=0x16b0 waiting for monitor entry [0x0000000021b1f000]
   java.lang.Thread.State: BLOCKED (on object monitor)
        at com.readBook.juc.DeadLock$2.run(DeadLock.java:30)
        - waiting to lock <0x000000076babf9c8> (a java.lang.String)
        - locked <0x000000076babf9f8> (a java.lang.String)
        at java.lang.Thread.run(Thread.java:748)

"Thread-0" #20 prio=5 os_prio=0 tid=0x00000000204a6800 nid=0x2864 waiting for monitor entry [0x0000000021a1f000]
   java.lang.Thread.State: BLOCKED (on object monitor)
        at com.readBook.juc.DeadLock$1.run(DeadLock.java:20)
        - waiting to lock <0x000000076babf9f8> (a java.lang.String)
        - locked <0x000000076babf9c8> (a java.lang.String)
        at java.lang.Thread.run(Thread.java:748)

可以看到线程1和线程0都在waiting to lock 并且自身都还有locked,说明是死锁。