Java 死锁的原理详解以及检测和解决死锁的方法

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。

详细介绍了Java死锁的概念,构建、排查、以及解决办法,帮助程序员了解死锁。

1 死锁概述

两个或者多个线程互相持有对方所需要的资源(锁),都在等待对方执行完毕才能继续往下执行的时候,就称为发生了死锁。结果就是两个进程都陷入了无限的等待中。一般是有多个锁对象的情况下并且获得锁顺序不一致造成的。

比如两条线程分别使用两个锁A、B,线程一是获得锁顺序是A、B,线程二获得锁顺序是B、A。线程一获得锁A后释放cpu执行权。线程二获得锁B后释放cpu执行权。它们持有对方继续执行所需要的锁,也不能主动释放自己持有的锁,此时发生死锁!死锁的底层是基于锁的特性产生的,基本上锁只能同时被一个线程持有,后续线程将进入等待队列对待!要了解锁的底层,需要JVM方面的知识,以及并发的知识,本文不作介绍。

2 死锁的产生的必要条件

死锁产生有四个必要条件,只要系统发生死锁则以上四个条件都必须成立。

  1. 互斥条件: 资源是独占的且排他使用,线程互斥使用资源,即任意时刻一个资源只能给一个线程使用,其他线程若申请一个资源,而该资源被另一线程占有时,则申请者等待直到资源被占有者释放。
  2. 不可剥夺条件: 线程所获得的资源在未使用完毕之前,不被其他线程强行剥夺,而只能由获得该资源的线程资源释放。
  3. 请求和保持条件: 线程每次申请它所需要的资源,在申请新的资源的同时,继续占用已分配到的资源。
  4. 循环等待条件: 在发生死锁时必然存在一个线程等待队列{P1,P2,…,Pn},其中P1等待P2占有的资源,P2等待P3占有的资源,…,Pn等待P1占有的资源,形成一个线程等待环路,环路中每一个线程所占有的资源同时被另一个申请,也就是前一个线程占有后一个线程所申请的资源。

事实上循环等待的成立蕴含了前三个条件的成立,似乎没有必要列出然而考虑这些条件对死锁的预防是有利的,因为可以通过破坏四个条件中的任何一个来预防死锁的发生。并且这四个条件本人觉得仅仅是帮助加深理解死锁的产生而已,大家理解记忆就行!

3 构建死锁

下面构建了一个死锁样例,主要是由于获取资源(锁)的顺序不一致导致的。

public class DeadLock {
    static ReentrantLock resource1 = new ReentrantLock();
    static ReentrantLock resource2 = new ReentrantLock();

    public static void main(String[] args) {
        deadLock();

    }

    /**
     * 构建两个线程,让他们发生死锁
     */
    private static void deadLock() {
        new Thread(() -> {
            while (true) {
                try {
                    resource1.lock();
                    System.out.println(Thread.currentThread().getName() + ": locked resource1");
                    try {
                        resource2.lock();
                        System.out.println(Thread.currentThread().getName() + ": locked resource2");
                    } finally {
                        resource2.unlock();
                    }
                } finally {
                    resource1.unlock();
                }
            }
        }, "deadLock-1").start();
        new Thread(() -> {
            while (true) {
                try {
                    resource2.lock();
                    System.out.println(Thread.currentThread().getName() + ": locked resource2");
                    try {
                        resource1.lock();
                        System.out.println(Thread.currentThread().getName() + ": locked resource1");
                    } finally {
                        resource1.unlock();
                    }
                } finally {
                    resource2.unlock();
                }
            }
        }, "deadLock-2").start();
    }

}
复制代码

运行一段时间后可以发现,控制台不再输出。

4 死锁检测

遇到死锁问题的时候,我们很容易觉得莫名其妙,而且定位问题也很困难。一般发生死锁时,主要表现为相关线程不再工作,但是并没有抛出异常。

下面三个方法可以帮我们检测死锁。

4.1 jstack

jstack命令用于生成虚拟机当前时刻的线程快照。

扫描二维码关注公众号,回复: 13168632 查看本文章

线程快照就是当前虚拟机内每一条线程正在执行的方法堆栈集合,生成线程快照的主要目的是定位线程出现长时间停顿的原因,如线程死锁、死循环、请求外部资源导致长时间等待等。jstack相比于jconsole更加的原始,没有图形化界面。

jstack 格式: jstack [option] vmid

控制台输入 jps 找到发生死锁的进程

在这里插入图片描述

控制台输入 jstack -l 108(108为死锁进程的pid)。出现线程堆栈信息,拉到最下面,即可发现死锁的信息。我们可以看到,这两个线程互相持有对方所需的资源,又互相需要对方的资源,因此发生了死锁。

在这里插入图片描述

4.2 Jconsole

从Java 5开始 引入了 JConsole,JConsole 是一个内置 Java 性能分析器。我们可以JConsole(或者,更高端的升级版jvisualvm)来监控 Java 应用程序性能和跟踪 Java 中的代码,包括死锁检测。

打开cmd窗口,运行 Jconsole命令。可以看到弹出Jconsole窗口,选择本地进程,选择发生死锁的进程,点击连接,如果出现”安全连接失败”,那么就选择”不安全的连接”。

在这里插入图片描述

点击”线程”,然后点击”死锁检测”。即可检测出发生死锁的线程

在这里插入图片描述

点击选择一个死锁线程,右边弹出具体信息,互相切换.即可查看死锁信息

在这里插入图片描述 在这里插入图片描述

4.3 jvisualvm

jvisualvm是Jconsole的高端升级版。

控制台输入 jvisualvm,将弹出图形化界面。

在左侧找到发生死锁的进程,双击,右边会弹出检测结果。

在这里插入图片描述

点击线程,我们可以看到这里自动提示了”检测到死锁”还是比较智能的,点击线程Dump查看具体死锁信息

在这里插入图片描述

**把信息拉倒最下面,发现了死锁的详细信息,与前面的一致,这里就不再介绍

在这里插入图片描述

5 解决死锁

常见解决死锁的方法里有加锁顺序一致、请求加锁时限、等待中断等甚至采用无锁编程等方法。

5.1 统一加锁顺序

使用多线程的时候,一种非常简单的避免死锁的方式就是:统一各个线程获取锁的顺序,并强制线程按照指定的顺序获取锁。因此,如果所有的线程都是以同样的顺序获得锁和释放锁,就不会出现死锁了。这需要编程人员的细心。

    /**
     * 解锁方法1:加锁顺序一致
     */
    private static void unLock1() {
        new Thread(() -> {
            while (true) {
                try {
                    resource1.lock();
                    System.out.println(Thread.currentThread().getName() + ": locked resource1");
                    try {
                        resource2.lock();
                        System.out.println(Thread.currentThread().getName() + ": locked resource2");
                    } finally {
                        resource2.unlock();
                    }
                } finally {
                    resource1.unlock();
                }
            }
        },"deadLock-1").start();
        new Thread(() -> {
            while (true) {
                try {
                    resource1.lock();
                    System.out.println(Thread.currentThread().getName() + ": locked resource1");
                    try {
                        resource2.lock();
                        System.out.println(Thread.currentThread().getName() + ": locked resource2");
                    } finally {
                        resource2.unlock();
                    }
                } finally {
                    resource1.unlock();
                }
            }
        },"deadLock-2").start();
    }
复制代码

5.2 请求锁时限&失败返回

另外一个可以避免死锁的方法是在尝试获取锁的时候加一个超时时间,这也就意味着在尝试获取锁的过程中若超过了这个时限该线程则放弃对该锁请求。若一个线程没有在给定的时限内成功获得所有需要的锁,则会进行回退并释放所有已经获得的锁,然后等待一段随机的时间再重试。

当然synchronized不具备这个功能,但是我们可以使用jdk1.5提供的Lock锁体系中的tryLock方法去尝试获取锁,这个方法可以指定一个超时时限,在等待超过该时限之后变回返回一个失败信息。

LOCK锁,jdk1.5之后提供的新锁! 比使用 synchronized 方法和语句可获得的更广泛的锁定操作! 此实现允许更灵活的结构,具有尝试获取锁的方法,如下方法:

tryLock()
尝试获取一把锁,如果获取成功返回true,如果还拿不到锁,就返回false。
如果锁可用,则获取锁,并立即返回值 true。
如果锁不可用,则此方法将立即返回值 false。
tryLock(long time, TimeUnit unit)
超时失效锁;这个方法在拿不到锁时会等待一定的时间,在时间期限之内如果还拿不到锁,就返回false。如果一开始拿到锁或者在等待期间内拿到了锁,则返回true。
如果锁可用,则此方法将立即返回值 true。
如果当前线程:在进入此方法时已经设置了该线程的中断状态;或者在获取锁时被中断,并且支持对锁获取的中断则将抛出 InterruptedException,并会清除当前线程的已中断状态。
如果超过了指定的等待时间,则将返回值 false。如果 time 小于等于 0,该方法将完全不等待。
复制代码

示例:

    /**
     * 解锁方法2:请求锁加上时间限制
     * tryLock方法,允许对锁的请求加上时间限制,这样,一定时间没有获取到锁的线程或直接返回,而不会发生死锁
     */
    private static void unLock2() {
        //jdk1.8
        new Thread(() -> {
            while (true) {
                try {
                    if (resource1.tryLock(3, TimeUnit.SECONDS)) {
                        System.out.println(Thread.currentThread().getName() + ": locked resource1");
                        if (resource2.tryLock(3, TimeUnit.SECONDS)) {
                            System.out.println(Thread.currentThread().getName() + ": locked resource2");
                            resource2.unlock();
                            resource1.unlock();
                        }else{
                            resource1.unlock();
                        }
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"deadLock-1").start();

        new Thread(() -> {
            while (true) {
                try {
                    if (resource2.tryLock(3, TimeUnit.SECONDS)) {
                        System.out.println(Thread.currentThread().getName() + ": locked resource2");
                        if (resource1.tryLock(3, TimeUnit.SECONDS)) {
                            System.out.println(Thread.currentThread().getName() + ": locked resource1");
                            resource1.unlock();
                            resource2.unlock();
                        }else {
                            resource2.unlock();
                        }
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"deadLock-2").start();
    }
复制代码

我们从打印结果可以看到,使用此方法,不再产生死锁,但是控制台输出缓慢,影响效率,因此我们可以使用trylock()方法,该方法没有请求等待时间,失败直接返回,效率更高,在此次不再演示。

5.3 等待中断

public class InterruptLock implements Runnable{
    public static ReentrantLock lock1 = new ReentrantLock();
    public static ReentrantLock lock2 = new ReentrantLock();
    int lock;

    public InterruptLock(int lock) {
        this.lock = lock;
    }

    @Override
    public void run() {
        try {
            if (lock == 1) {
                lock1.lockInterruptibly();
                Thread.sleep(500);
                lock2.lockInterruptibly();
            } else {
                lock2.lockInterruptibly();
                Thread.sleep(500);
                lock1.lockInterruptibly();
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            if (lock1.isHeldByCurrentThread()) {
                lock1.unlock();
            }
            if (lock2.isHeldByCurrentThread()) {
                lock2.unlock();
            }
            System.out.println(Thread.currentThread().getId() + ":线程退出");
        }

    }

    /**
     * 中断响应lockInterruptibly
     *
     * @param args
     * @throws InterruptedException
     */
    public static void main(String args[]) throws InterruptedException {
        InterruptLock r1 = new InterruptLock(1);
        InterruptLock r2 = new InterruptLock(2);

        Thread thread1 = new Thread(r1);
        Thread thread2 = new Thread(r2);

        thread1.start();
        thread2.start();

        Thread.sleep(1000);

        thread2.interrupt();

    }
}
复制代码

在main方法中,主线程main处于休眠,此时,这两个线程处于死锁的状态,在下一行,由于t2线程被中断, 故t2会放弃对lock1的申请, 同时释放已获得lock2。 这个操作导致ti 线程可以顺利得到lock2而继续执行下去。最终结果是:

java.lang.InterruptedException
	at java.util.concurrent.locks.AbstractQueuedSynchronizer.doAcquireInterruptibly(AbstractQueuedSynchronizer.java:898)
	at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireInterruptibly(AbstractQueuedSynchronizer.java:1222)
	at java.util.concurrent.locks.ReentrantLock.lockInterruptibly(ReentrantLock.java:335)
	at com.thread.test.deadlock.InterruptLock.run(InterruptLock.java:33)
	at java.lang.Thread.run(Thread.java:748)
13:线程退出
12:线程退出
复制代码

可以看到中断后,两个线程双双退出。但真正完成工作的只有ti。而t2线程则放弃其任务直接退出,释放资源。

5.4无锁编程

想要彻底没有死锁,我们还可以使用无锁编程的方法,Java中的无锁编程 ,主要是使用CAS方法,无锁编程的难度比较高,所以该方法的适用广度还是比较窄的,但是它的效率更高。基于JDK1.8中的JUC包中的很多类,都是用无锁编程算法,比如Atomic包下的全部原子类、并发集合等,我们可以使用现成的类库来进行编程。

关于CAS的原理:Java CAS操作的实现原理深度解析与应用案例

还有一点就是,使用CAS无锁编程的时,很多时候我们是直接操作的是对象的指针(内存地址),因为我们需要根据对象的字段的内存地址来更新数据。我们知道,直接操作指针是非常危险的,包括C、C++,java中则直接把指针的概念“取消了”,不过我们还是可以通过Unsafe类来操作对象内存(具体介绍可以看我的博客Java-并发-JUC中的Unsafe类),但是稍微不注意就可能造成程序崩溃,这也是我们自己实现CAS无锁编程的难点。

不过很多有名的类库或者组件底层都是用CAS,比如kafka,netty,我们可以多看看他们的源码,来提升自己的理解。

5 总结

对于不进行多线程编程的程序员来说,死锁可能基本上遇不到;对于经常进程多线程编程的程序员来说,死锁可能也很少遇到(^_^,实话,我同事基本没遇到过)。不过咱们还是应该简单学习死锁的知识,如果后面真的遇到了,我们能很快的解决,那也不失一种好的表现的机会,况且,面试也有可能问到不是吗?

如果有什么不懂或者需要交流,可以留言。另外希望点赞、收藏、关注,我将不间断更新各种Java学习博客!

猜你喜欢

转载自juejin.im/post/7019476990302355470