zoukankan      html  css  js  c++  java
  • 面试题:写一个死锁示例

    前言

      我们首先需要知道,死锁一定发生在并发场景中。为了保证线程安全,有时会给程序使用各种能保证并发安全的工具,尤其是锁,但是如果在加解锁过程中处理不恰当,就有可能适得其反,导致程序出现死锁的情况。

    什么是死锁

      什么是死锁?死锁是这样一种情形:多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止。

      Java应用程序死锁产生的四个必要条件:
      1、互斥使用,即当资源被一个线程使用(占有)时,别的线程不能使用。
      2、不可抢占,资源请求者不能强制从资源占有者手中夺取资源,资源只能由资源占有者主动释放。
      3、请求和保持,即当资源请求者在请求其它资源的同时保持对原有资源的占有。
      4、循环等待,即存在一个等待队列:P1占有P2的资源,P2占有P3的资源,P3占有P1的资源。这样就形成了一个等待闭环。

      当上述四个条件都成立的时候,便形成死锁。当然,如果打破上述任何一个条件,便可终结死锁。

      解决死锁问题的方法常用的有三种:一种是用synchronized,一种是用Lock显式锁实现,另一种是在分布式环境中加分布式锁。

      死锁是一种状态,当两个(或多个)线程(或进程)相互持有对方所需要的资源,却又都不主动释放自己手中所持有的资源,导致大家都获取不到自己想要的资源,所有相关的线程(或进程)都无法继续往下执行,在未改变这种状态之前都不能向前推进,我们就把这种状态称为死锁状态,认为它们发生了死锁。简而言之,死锁就是两个或多个线程(或进程)被无限期地阻塞,相互等待对方手中资源的一种状态。

          图1 两个线程死锁的情况

      如图所示,线程1已经持有了锁1,同时线程2也已经持有了锁2,然后线程1尝试获取锁2,但是线程2并没有释放锁2,所以线程1处于阻塞状态;同理可得,线程2获取锁1也会被阻塞。由此可见,线程1和线程2就发生了死锁,因为它们都相互持有对方想要的资源,却又不释放自己手中的资源,形成相互等待,而且会一直等待下去。

    2 死锁的影响和危害

    2.1 死锁的影响

      死锁的影响在不同系统中是不一样的,影响的大小一部分取决于当前这个系统或者环境对死锁的处理能力。

    2.1.1 数据库中

      例如,在数据库系统软件的设计中,考虑了监测死锁以及从死锁中恢复的情况。在执行一个事务的时候可能需要获取多把锁,并一直持有这些锁直到事务完成。在某个事务中持有的锁可能在其它事务中也需要,因此在两个事务之间有可能发生死锁的情况;一旦发生了死锁,如果没有外部干涉,那么两个事务就会永远的等待下去。

      但数据库系统不会放任这种情况发生,当数据库检测到这一组事务发生了死锁时,根据策略的不同,可能会选择放弃某一个事务,被放弃的事务就会释放掉它所持有的锁,从而使其它的事务继续顺利进行。此时程序可以重新执行被强行终止的事务,而这个事务现在就可以顺利执行了,因为所有跟它竞争资源的事务都已经在刚才执行完毕,并且释放资源了。

    2.1.2 JVM 中

      在 JVM 中,对于死锁的处理能力就不如数据库那么强大了。如果在 JVM 中发生了死锁,JVM 并不会自动进行处理,所以一旦死锁发生,就会陷入无穷无尽的等待。

    2.2 死锁的危害以及特点

      死锁和其它的并发安全问题一样,是概率性事件,也就是说,即使存在发生死锁的可能性,也并不是 100% 会发生的。如果每个锁的持有时间很短,那么发生冲突的概率就很低,所以死锁发生的概率也很低。但是在线上系统里,可能每天有几千万次的“获取锁”、"释放锁”操作,在巨量的加解锁过程中,整个系统发生问题的概率就会被放大,只要有某几次操作是有风险的,就可能会导致死锁的发生。

      也正是因为死锁“不一定会发生”的特点,导致提前找出死锁成为了一个难题。压力测试虽然可以检测出一部分可能发生死锁的情况,但是并不足以完全模拟真实、长期运行的场景,因此没有办法把所有潜在可能发生死锁的代码都找出来。

      死锁还会导致产生新的死锁,像滚雪球一样,越滚越大。其它线程因请求不到死锁进程已占用的资源而无法向前推进,也会发生死锁。所以如果线程出现了死锁,有可能产生多米诺骨牌效应,最终会导致操作系统崩溃。

      一旦发生了死锁,根据发生死锁的线程的职责不同,就可能会造成 子系统崩溃、性能降低甚至整个系统崩溃等各种不良后果。而且死锁往往发生在高并发、高负载的情况下,因为可能会直接影响到很多用户,造成一系列的问题。以上就是死锁发生几率不高但是危害大的特点。

    3 必然死锁示例

      在实际编程中,要尽量避免出现死锁的情况,但是让你故意写一个死锁的程序时似乎也不太简单(有公司会出这样的面试题)。下面是用java代码来模拟一个简单的、产生死锁的例子,思路是创建两个字符串obj1和obj2,再创建两个线程la和lb,让每个线程都用synchronized关键字锁住字符串(la先锁obj1,再去锁obj2;lb先锁obj2,再锁obj1),如果la锁住obj1,lb锁住obj2,la就没办法锁住obj2,lb也没办法锁住obj1,这时就陷入了死锁。代码如下:

    import java.util.Date;
     
    public class LockTest {
       public static String obj1 = "obj1";
       public static String obj2 = "obj2";
       public static void main(String[] args) {
          LockA la = new LockA();
          new Thread(la).start();
          LockB lb = new LockB();
          new Thread(lb).start();
       }
    }
    class LockA implements Runnable{
       public void run() {
          try {
             System.out.println(new Date().toString() + " LockA 开始执行");
             while(true){
                synchronized (LockTest.obj1) {
                   System.out.println(new Date().toString() + " LockA 锁住 obj1");
                   Thread.sleep(3000); // 此处等待是给B能锁住机会
                   synchronized (LockTest.obj2) {
                      System.out.println(new Date().toString() + " LockA 锁住 obj2");
                      Thread.sleep(60 * 1000); // 为测试,占用了就不放
                   }
                }
             }
          } catch (Exception e) {
             e.printStackTrace();
          }
       }
    }
    class LockB implements Runnable{
       public void run() {
          try {
             System.out.println(new Date().toString() + " LockB 开始执行");
             while(true){
                synchronized (LockTest.obj2) {
                   System.out.println(new Date().toString() + " LockB 锁住 obj2");
                   Thread.sleep(3000); // 此处等待是给A能锁住机会
                   synchronized (LockTest.obj1) {
                      System.out.println(new Date().toString() + " LockB 锁住 obj1");
                      Thread.sleep(60 * 1000); // 为测试,占用了就不放
                   }
                }
             }
          } catch (Exception e) {
             e.printStackTrace();
          }
       }
    }
    

      运行以上代码后,控制台输出结果为:

    Tue May 05 10:51:06 CST 2015 LockB 开始执行
    Tue May 05 10:51:06 CST 2015 LockA 开始执行
    Tue May 05 10:51:06 CST 2015 LockB 锁住 obj2
    Tue May 05 10:51:06 CST 2015 LockA 锁住 obj1

      此时死锁产生。在 JVM 中如果发生死锁,可能会导致程序部分甚至全部功能无法继续向下执行的情况,所以死锁在 JVM 中所带来的危害和影响是比较大的,我们需要尽量避免。

    Reference


      读后有收获,小礼物走一走,请作者喝咖啡。

    赞赏支持

  • 相关阅读:
    4月13日
    java线程池开启多线程
    cenos 7 zookeeper Error contacting service. It is probably not running
    io.lettuce.core.RedisCommandTimeoutException: Command timed out
    springboot java jar指定启动的jar外部配置文件
    ApplicationContextAware
    YYYYMMdd和yyyyMMdd的区别
    gpg加解密异常
    第四周学习及读书笔记
    第3章 直流电机的工作原理及特性 学习笔记(二)
  • 原文地址:https://www.cnblogs.com/east7/p/14822936.html
Copyright © 2011-2022 走看看