死锁的发生,定位与修复

阅读:292

目录介绍

  • 1.代码模拟产生死锁
    • 1.1 代码描述
    • 1.2 产生死锁代码
    • 1.3 死锁发生的场景
  • 2.Java中导致死锁的原因
    • 2.1 造成死锁原因
    • 2.2 死锁的危害
  • 3.出现死锁需要满足条件
    • 3.1 死锁问题条件
    • 3.2 如何预防死锁
  • 4.死锁诊断步骤
    • 4.1 如何定位死锁
  • 5.死锁修复解决方案
    • 5.1 死锁修复方案
  • 6.手写死锁代码

首先思考问题

  • 死锁的概念和产生死锁的根本原因是什么?死锁的预防策略中资源有序分配策略是什么。
  • 理解产生死锁的必要条件--以下四个条件同时具备:互斥条件、不可抢占条件、占有且申请条件、循环等待条件。这几个条件之间有什么关系。
  • 死锁的检测及恢复的思想。死锁是如何解决和避免的。结合具体案例分析一下思路?
  • Android实际开发中是否有遇到过死锁,在什么情景下遇到的,当时是怎么排查问题并且解决的?

1.代码模拟产生死锁

1.1 代码描述

  • 启动两个线程,设置两个线程监听对象obj1 、obj2。
    • 线程1启动的时候,先获取obj1锁,暂停1秒,然后获取obj2锁。
    • 线程2启动时,先获取obj2,再获取obj1
  • 当线程2启动的时候获取obj2成功,然后去获取obj1的时候,obj1被线程1占用,此时就等待。线程1秒后去获取obj2,此时obj2锁被线程2握着,产生死锁,互相无法获取。

1.2 产生死锁代码

  • 代码如下所示
    • 这种方法将直接导致死锁
    private final Object obj1 = new Object();
    private final Object obj2 = new Object();
    private void test1() {
        new Thread(){
            @Override
            public void run() {
                synchronized (obj1){
                    try {
                        System.out.println("yc---Thread1 obj1");
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    synchronized (obj2){
                        System.out.println("yc---Thread1 obj2");
                    }
                }
            }
        }.start();
        new Thread(){
            @Override
            public void run() {
                synchronized (obj2){
                    System.out.println("yc---Thread2 obj2");
                    synchronized (obj1){
                        System.out.println("yc---Thread2 obj1");
                    }
                }
            }
        }.start();
    }
    
    

  • 接下来去掉上面代码中的Thread.sleep(1000)代码
    • 注意去掉了这行代码后,开始时可以正常打印日志,多次触发test1()方法后,发现最终也会出现死锁问题。
    private final Object obj1 = new Object();
    private final Object obj2 = new Object();
    private void test1() {
        new Thread(){
            @Override
            public void run() {
                synchronized (obj1){
                    System.out.println("yc---Thread1 obj1");
                    synchronized (obj2){
                        System.out.println("yc---Thread1 obj2");
                    }
                }
            }
        }.start();
        new Thread(){
            @Override
            public void run() {
                synchronized (obj2){
                    System.out.println("yc---Thread2 obj2");
                    synchronized (obj1){
                        System.out.println("yc---Thread2 obj1");
                    }
                }
            }
        }.start();
    }
    
    

1.3 死锁发生的场景

  • 死锁不仅仅是在线程之间会发生,存在资源独占的进程之间同样也可能出现死锁。大多是聚焦在多线程场景中的死锁,指两个或多个线程之间,由于互相持有对方需要的锁,而永久处于阻塞的状态。

2.Java中导致死锁的原因

2.1 造成死锁原因

  • 在申请锁时发生了交叉闭环申请。即线程在获得了锁1并且没有释放的情况下去申请锁2,这时,另一个线程已经获得了锁2,在释放锁2之前又要先获得锁1,因此闭环发生,陷入死锁循环。

1.png

2.2 死锁的危害

  • 从上面死锁代码案例可以知道,当发生死锁的时候,导致彼此一直处于等待之中,而导致代码无法执行下去。只能重启,后果比较严重!
  • 在死锁时,线程间相互等待资源,而又不释放自身的资源,导致无穷无尽的等待,其结果是系统任务永远无法执行完成。系统发生死锁现象不仅浪费大量的系统资源,甚至导致整个系统崩溃,带来灾难性后果。

3.出现死锁需要满足条件

3.1 死锁问题条件

    1. 互斥条件:一个资源每次只能被一个线程使用。
    1. 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
    1. 不剥夺条件:进程已获得的资源,在未使用完之前,不能强行剥夺。
    1. 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。

3.2 如何预防死锁

  • 死锁发生时的四个必要条件,只要破坏这四个必要条件中的任意一个条件,死锁就不会发生。这就为我们解决死锁问题提供了可能。一般地,解决死锁的方法分为死锁的预防,避免,检测[定位死锁的位置]与恢复三种(注意:死锁的检测与恢复是一个方法)。 锁的预防是保证系统不进入死锁状态的一种策略。它的基本思想是要求进程申请资源时遵循某种协议,从而打破产生死锁的四个必要条件中的一个或几个,保证系统不会进入死锁状态。
    • 打破互斥条件。即允许进程同时访问某些资源。但是,有的资源是不允许被同时访问的,像打印机等等,这是由资源本身的属性所决定的。所以,这种办法并无实用价值。
    • 打破不可抢占条件。即允许进程强行从占有者那里夺取某些资源。就是说,当一个进程已占有了某些资源,它又申请新的资源,但不能立即被满足时,它必须释放所占有的全部资源,以后再重新申请。它所释放的资源可以分配给其它进程。这就相当于该进程占有的资源被隐蔽地强占了。这种预防死锁的方法实现起来困难,会降低系统性能。
    • 打破占有且申请条件。可以实行资源预先分配策略。即进程在运行前一次性地向系统申请它所需要的全部资源。如果某个进程所需的全部资源得不到满足,则不分配任何资源,此进程暂不运行。只有当系统能够满足当前进程的全部资源需求时,才一次性地将所申请的资源全部分配给该进程。由于运行的进程已占有了它所需的全部资源,所以不会发生占有资源又申请资源的现象,因此不会发生死锁。但是,这种策略也有如下缺点:
      • 在许多情况下,一个进程在执行之前不可能知道它所需要的全部资源。这是由于进程在执行时是动态的,不可预测的;
      • 资源利用率低。无论所分资源何时用到,一个进程只有在占有所需的全部资源后才能执行。即使有些资源最后才被该进程用到一次,但该进程在生存期间却一直占有它们,造成长期占着不用的状况。这显然是一种极大的资源浪费;
      • 降低了进程的并发性。因为资源有限,又加上存在浪费,能分配到所需全部资源的进程个数就必然少了。
    • 打破循环等待条件,实行资源有序分配策略。采用这种策略,即把资源事先分类编号,按号分配,使进程在申请,占用资源时不会形成环路。所有进程对资源的请求必须严格按资源序号递增的顺序提出。进程占用了小号资源,才能申请大号资源,就不会产生环路,从而预防了死锁。这种策略与前面的策略相比,资源的利用率和系统吞吐量都有很大提高,但是也存在以下缺点:
      • 限制了进程对资源的请求,同时给系统中所有资源合理编号也是件困难事,并增加了系统开销;
      • 为了遵循按编号申请的次序,暂不使用的资源也需要提前申请,从而增加了进程对资源的占用时间。

4.死锁诊断步骤

4.1 如何定位死锁

  • 定位死锁最常用的工具就是利用jstack等工具获取线程栈,然后定位相互之间的依赖关系,进而找到死锁。如果是比较明显的死锁,往往jstack工具就能直接定位,类似JConsole甚至可以在图形界面进行有限的死锁检测。
  • 如果程序运行时发生了死锁,绝大多数情况下都是无法在线解决的,只能重启、修正程序本身问题。所以,代码开发阶段相互审查,或者利用工具进行预防性排查,也是很重要的。
    • 1.首先,可以使用 jps 或者系统的 ps 命令、任务管理器等工具,确定进程 ID。
    • 2.调用 jstack 获取线程栈:jstack your-pid
    • 3.然后看看日志,思考怎么用studio将日志打印出来呢?

5.死锁修复解决方案

5.1 死锁修复方案

  • 如果在死锁检查时发现了死锁情况,那么就要努力消除死锁,使系统从死锁状态中恢复过来。消除死锁的几种方式:
    • 1.最简单、最常用的方法就是进行系统的重新启动,不过这种方法代价很大,它意味着在这之前所有的进程已经完成的计算工作都将付之东流,包括参与死锁的那些进程,以及未参与死锁的进程;
    • 2.撤消进程,剥夺资源。终止参与死锁的进程,收回它们占有的资源,从而解除死锁。这时又分两种情况:一次性撤消参与死锁的全部进程,剥夺全部资源;或者逐步撤消参与死锁的进程,逐步收回死锁进程占有的资源。一般来说,选择逐步撤消的进程时要按照一定的原则进行,目的是撤消那些代价最小的进程,比如按进程的优先级确定进程的代价;考虑进程运行时的代价和与此进程相关的外部作业的代价等因素;
    • 3.进程回退策略,即让参与死锁的进程回退到没有发生死锁前某一点处,并由此点处继续执行,以求再次执行时不再发生死锁。虽然这是个较理想的办法,但是操作起来系统开销极大,要有堆栈这样的机构记录进程的每一步变化,以便今后的回退,有时这是无法做到的。

6.手写死锁代码

  • 死锁的简单代码
    • 思路是创建两个字符串a和b,再创建两个线程A和B,让每个线程都用synchronized锁住字符串(A先锁a,再去锁b;B先锁b,再锁a),如果A锁住a,B锁住b,A就没办法锁住b,B也没办法锁住a,这时就陷入了死锁。
    • 打印结果:可以看到,Lock1获取obj1,Lock2获取obj2,但是它们都没有办法再获取另外一个obj,因为它们都在等待对方先释放锁,这时就是死锁。
    public class DeadLock {
        public static String obj1 = "obj1";
        public static String obj2 = "obj2";
        public static void main(String[] args){
            Thread a = new Thread(new Lock1());
            Thread b = new Thread(new Lock2());
            a.start();
            b.start();
        }    
    }
    class Lock1 implements Runnable{
        @Override
        public void run(){
            try{
                System.out.println("Lock1 running");
                while(true){
                    synchronized(DeadLock.obj1){
                        System.out.println("Lock1 lock obj1");
                        Thread.sleep(3000);//获取obj1后先等一会儿,让Lock2有足够的时间锁住obj2
                        synchronized(DeadLock.obj2){
                            System.out.println("Lock1 lock obj2");
                        }
                    }
                }
            }catch(Exception e){
                e.printStackTrace();
            }
        }
    }
    class Lock2 implements Runnable{
        @Override
        public void run(){
            try{
                System.out.println("Lock2 running");
                while(true){
                    synchronized(DeadLock.obj2){
                        System.out.println("Lock2 lock obj2");
                        Thread.sleep(3000);
                        synchronized(DeadLock.obj1){
                            System.out.println("Lock2 lock obj1");
                        }
                    }
                }
            }catch(Exception e){
                e.printStackTrace();
            }
        }
    }
    


    • 如果我们只运行Lock1呢?修改一下main函数,把线程b注释掉。

赞赏支持


精彩留言

发表评论