zoukankan      html  css  js  c++  java
  • 发生死锁怎么办

    锁的定义:死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。竞争的资源可以是:锁、网络连接、磁盘共享变量等一切可以称作是 【资源】的东西。

    我们使用锁来保证线程安全,但是使用不当与滥用可能就会引起死锁。并发程序一旦死锁,一般没有特别好的办法,很多时候只能重启。所以我们一定要比避免死锁。

    简单例子

    举个不恰当的例子:现在岳不群通过阴谋手段获取到了葵花宝典的上册,然后就闭关修炼自宫了,此刻他想继续争夺下册一块练,不然自宫就白忙活了。这个时候下册被林平之拿到了,他也要修炼葵花宝典,所以藏着下册去找上册来自宫。现在问题来了,岳不群找不到下册。林平之拿不到上册,两个人就只能干瞪眼谁也不肯交出自己的,同事还要获取对方的。

    如果此时有一个线程 A ,按照先获持有锁 a 再获取锁 b的顺序获得锁,同时另外一个线程 B,按照先获取锁 b 再获取锁 a 的顺序获取锁。如下图所示:
    死锁

    接着我们用代码模拟上线的执行过程,默认使用 SpringBoot 环境

    @Component
    public class DeadLock {
        private static Object lockA = new Object();
        private static Object lockB = new Object();
    
        public void deadLock() {
            Thread threadA = new Thread(() -> {
                synchronized (lockA) {
                    System.out.println(Thread.currentThread().getName() + "获取 lockA 成功");
                    try {
                        TimeUnit.SECONDS.sleep(1);
                        System.out.println(Thread.currentThread().getName() + "尝试获取 lockB ");
                        synchronized (lockB) {
                            System.out.println(Thread.currentThread().getName() + "获取 lockB 成功");
                        }
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
    
                }
            });
            Thread threadB = new Thread(() -> {
                synchronized (lockB) {
                    System.out.println(Thread.currentThread().getName() + "获取 lockB 成功 ");
                    try {
                        TimeUnit.SECONDS.sleep(1);
                        System.out.println(Thread.currentThread().getName() + "尝试获取 lockA ");
                        synchronized (lockA) {
                            System.out.println(Thread.currentThread().getName() + "获取 lockA 成功");
                        }
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
    
                }
            });
    
            threadA.start();
            threadB.start();
        }
    
    }
    

    单元测试

    @RunWith(SpringRunner.class)
    @SpringBootTest
    public class DemoApplicationTests {
        @Autowired
        private DeadLock deadLock;
    
        @Test
        public void contextLoads() {
            deadLock.deadLock();
            try {
                Thread.currentThread().join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    
    }
    

    控制台打印

    Thread-4获取 lockB 成功 
    Thread-3获取 lockA 成功
    Thread-3尝试获取 lockB 
    Thread-4尝试获取 lockA 
    

    我们可以发现 Thread-3 获取 lockA 成功后尝试获取 lockB 一直不能成功。相互等待对方释放形成了死锁。

    死锁检查

    jstack 指令

    该指令可以生成虚拟机当前时刻的线程快照。线程快照是当前每一条线程正在执行的方法对战的集合,主要目的是定位线程出现长时间停顿的原因,比如 线程间死锁死循环请求外部资源导致的长时间等待等。

    先通过 jps 获取正在执行的进程 id。

    $ jps
    23264 Jps
    8472 JUnitStarter
    
    

    再用jstack 查看当前进程的堆栈信息

    $ jstack -F 8472
    Attaching to process ID 8472, please wait...
    Debugger attached successfully.
    Server compiler detected.
    JVM version is 25.181-b13
    Deadlock Detection:
    
    Found one Java-level deadlock:
    =============================
    
    "Thread-4":
      waiting to lock Monitor@0x000000001f0134f8 (Object@0x00000007721d90f0, a java/lang/Object),
      which is held by "Thread-3"
    "Thread-3":
      waiting to lock Monitor@0x000000001f011ef8 (Object@0x00000007721d90e0, a java/lang/Object),
      which is held by "Thread-4"
    
    Found a total of 1 deadlock.
    
    Thread 21: (state = BLOCKED)
     - com.zero.demo.deadlock.DeadLock.lambda$deadLock$1() @bci=79, line=35 (Interpreted frame)
     - com.zero.demo.deadlock.DeadLock$$Lambda$170.run() @bci=0 (Interpreted frame)
     - java.lang.Thread.run() @bci=11, line=748 (Interpreted frame)
    
    
    Thread 20: (state = BLOCKED)
     - com.zero.demo.deadlock.DeadLock.lambda$deadLock$0() @bci=79, line=20 (Interpreted frame)
     - com.zero.demo.deadlock.DeadLock$$Lambda$169.run() @bci=0 (Interpreted frame)
     - java.lang.Thread.run() @bci=11, line=748 (Interpreted frame)
    
    

    可以看到存在死锁 Found a total of 1 deadlock.

    死锁预防

    我们知道了死锁如何产生的,那么就知道该如何去预防。如果一个线程每次只能获取一个锁,那么就不会出现由于嵌套持有锁顺序导致的死锁。

    1. 正确的顺序获得锁

    如果必须获取多个锁,我们就要考虑不同线程获取锁的顺序。

    上面的例子出现死锁的根本原因就是获取所的顺序是乱序的,超乎我们控制的。上面例子最理想的情况就是把业务逻辑抽离出来,把获取锁的代码放在一个公共的方法里面,让这两个线程获取锁

    都是从我的公共的方法里面获取,当Thread1线程进入公共方法时,获取了A锁,另外Thread2又进来了,但是A锁已经被Thread1线程获取了,所以只能阻塞等待。Thread1接着又获取锁B,Thread2线程就不能再获取不到了锁A,更别说再去获取锁B了,这样就有一定的顺序了。只有当线程1释放了所有锁,线程B才能获取。

    比如前面的例子我们改成

    @Component
    public class DeadLock {
        private static Object lockA = new Object();
        private static Object lockB = new Object();
    
        public void deadLock() {
            Thread threadA = new Thread(() -> {
                getLock();
            });
            Thread threadB = new Thread(() -> {
                getLock();
            });
    
            threadA.start();
            threadB.start();
        }
    
        private void getLock() {
            synchronized (lockA) {
                System.out.println(Thread.currentThread().getName() + "获取 lockA 成功");
                try {
                    TimeUnit.SECONDS.sleep(1);
                    System.out.println(Thread.currentThread().getName() + "尝试获取 lockB ");
                    synchronized (lockB) {
                        System.out.println(Thread.currentThread().getName() + "获取 lockB 成功");
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
    
            }
        }
    
    }
    
    

    查看打印结果,我们发现 线程4 获取成功然后线程3才能继续获取。

    Thread-4获取 lockA 成功
    Thread-4尝试获取 lockB 
    Thread-4获取 lockB 成功
    Thread-3获取 lockA 成功
    Thread-3尝试获取 lockB 
    Thread-3获取 lockB 成功
    

    2. 超时放弃

    当线程获取锁超时了则放弃,这样就避免了出现死锁获取的情况。当使用synchronized关键词提供的内置锁时,只要线程没有获得锁,那么就会永远等待下去,然而Lock接口提供了boolean tryLock(long time, TimeUnit unit) throws InterruptedException方法,该方法可以按照固定时长等待锁,因此线程可以在获取锁超时以后,主动释放之前已经获得的所有的锁。通过这种方式,也可以很有效地避免死锁。

    其他死锁

    我们再来回顾一下死锁的定义,“死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。” 死锁条件里面的竞争资源,可以是线程池里的线程、网络连接池的连接,数据库中数据引擎提供的锁,等等一切可以被称作竞争资源的东西。

    线程池死锁

    final ExecutorService executorService = 
            Executors.newSingleThreadExecutor();
    Future<Long> f1 = executorService.submit(new Callable<Long>() {
    
        public Long call() throws Exception {
            System.out.println("start f1");
            Thread.sleep(1000);//延时
            Future<Long> f2 = 
               executorService.submit(new Callable<Long>() {
    
                public Long call() throws Exception {
                    System.out.println("start f2");
                    return -1L;
                }
            });
            System.out.println("result" + f2.get());
            System.out.println("end f1");
            return -1L;
        }
    });
    
    

    线程池类型是单一线程,但是任务1依赖任务2的执行结果,由于单线程模式,任务1没有执行完,任务2永远得不到执行,就死锁了。

    总结

    在我的理解当中,死锁就是“两个任务以不合理的顺序互相争夺资源”造成,因此为了规避死锁,应用程序需要妥善处理资源获取的顺序。 另外有些时候,死锁并不会马上在应用程序中体现出来,在通常情况下,都是应用在生产环境运行了一段时间后,才开始慢慢显现出来,在实际测试过程中,由于死锁的隐蔽性,很难在测试过程中及时发现死锁的存在,而且在生产环境中,应用出现了死锁,往往都是在应用状况最糟糕的时候——在高负载情况下。因此,开发者在开发过程中要谨慎分析每个系统资源的使用情况,合理规避死锁,另外一旦出现了死锁,也可以尝试使用本文中提到的一些工具,仔细分析,总是能找到问题所在的。

    关注公众号 JavaStorm 转发与点赞一起牛逼。

  • 相关阅读:
    文件读写和进度条
    复选框选择变化(可以演化成简单的字符串拼接)
    读取文本方式的简单登录
    计算字符出现次数
    判断系统版本号
    DataTable合并
    获取单元格值的数据类型
    struts2 日期标签
    jsp获取枚举的值
    java web项目修改项目名称
  • 原文地址:https://www.cnblogs.com/uniqueDong/p/11249383.html
Copyright © 2011-2022 走看看