• 纯知识分享||Java多线程又遇到死锁?三招帮你彻底解决


    有一天啊,小明和小丽两个人去玩密室逃脱,在游戏过程中分别被关到不同的房间里,小明身上有能打开小丽房间的钥匙,小丽身上有能打开小明房间的钥匙。

    然而小明想要出去救小丽,就得有小丽身上的钥匙,显然他得不到;小丽想要出去救小明,就得有小明身上的钥匙,显然她也做不到。

    这种情况在我们程序界被称为——死锁。

    那具体什么是死锁,为何出现,如果出现,该怎么解决呢?

     什么是死锁 

    多线程环境中,多个进程可以竞争有限数量的资源。当一个进程申请资源时,如果这时没有可用资源,那么这个进程进入等待状态。

    有时,如果所申请的资源被其他等待进程占有,那么该等待进程有可能再也无法改变状态。这种情况称为死锁

    在Java中使用多线程,就会有可能导致死锁问题。死锁会让程序一直卡住,不再往下执行。我们只能通过中止并重启的方式来让程序重新执行。

     造成死锁的原因 

    • 当前线程拥有其他线程需要的资源

    • 当前线程等待其他线程已拥有的资源

    • 都不放弃自己拥有的资源

     死锁的必要条件 

    1️⃣ 互斥

    进程要求对所分配的资源(如打印机)进行排他性控制,即在一段时间内某资源仅为一个进程所占有。此时若有其他进程请求该资源,则请求进程只能等待。

    2️⃣ 不可剥夺

    进程所获得的资源在未使用完毕之前,不能被其他进程强行夺走,即只能由获得该资源的进程自己来释放(只能是主动释放)。

    3️⃣ 请求与保持

    进程已经保持了至少一个资源,但又提出了新的资源请求,而该资源已被其他进程占有,此时请求进程被阻塞,但对自己已获得的资源保持不放。

    4️⃣ 循环等待

    是指进程发生死锁后,必然存在一个进程–资源之间的环形链,通俗讲就是你等我的资源,我等你的资源,大家一直等

     死锁的分类及解决方法 

     1️⃣ 静态顺序型死锁 

    线程之间形成相互等待资源的环时,就会形成顺序死锁lock-orderingdeadlock,多个线程试图以不同的顺序来获取相同的锁时,容易形成顺序死锁,如果所有线程以固定的顺序来获取锁,就不会出现顺序死锁问题。

    经典案例是LeftRightDeadlock,两个方法,分别是leftRigth、rightLeft。如果一个线程调用leftRight,另一个线程调用rightLeft,且两个线程是交替执行的,就会发生死锁。

    publicclassLeftRightDeadLock {

      //左边锁
      privatestaticObjectleft =newObject();
      //右边锁
      privatestaticObjectright =newObject();

      /**
       *现持有左边的锁,然后获取右边的锁
       */
      publicstaticvoidleftRigth(){
          synchronized(left){
              System.out.println("leftRigth:left lock,threadId:"+Thread.currentThread().getId());
              //休眠增加死锁产生的概率
              sleep(100);
              synchronized(right){
                  System.out.println("leftRigth:right lock,threadId:"+Thread.currentThread().getId());
              }
          }
      }

      /**
       *现持有右边的锁,然后获取左边的锁
       */
      publicstaticvoidrightLeft(){
          synchronized(right){
              System.out.println("rightLeft:right lock,threadId:"+Thread.currentThread().getId());
              //休眠增加死锁产生的概率
              sleep(100);
              synchronized(left){
                  System.out.println("rightLeft:left lock,threadId:"+Thread.currentThread().getId());
              }
          }
      }

      /**
       *休眠
       *
       * @paramtime
       */
      privatestaticvoidsleep(longtime){
          try{
              Thread.sleep(time);
          }catch(InterruptedExceptione){
              e.printStackTrace();
          }
      }

      publicstaticvoidmain(String[]args){
          //创建一个线程池
          ExecutorServiceexecutorService =Executors.newFixedThreadPool(10);
          executorService.execute(()->leftRigth());
          executorService.execute(()->rightLeft());
          executorService.shutdown();
      }
    }

    输出:

    leftRigth:left lock,threadId:12
    rightLeft:right lock,threadId:13

    我们发现,12号线程锁住了左边要向右边获取锁,13号锁住了右边,要向左边获取锁,因为两边都不释放自己的锁,互不相让,就产生了死锁。

      💡  解决方案

    固定加锁的顺序(针对锁顺序死锁)

    只要交换下锁的顺序,让线程来了之后先获取同一把锁,获取不到就等待,等待上一个线程释放锁再获取锁。

    publicstaticvoidleftRigth(){
         synchronized(left){
           ...
             synchronized(right){
              ...
             }
         }
     }

     publicstaticvoidrightLeft(){
         synchronized(left){
           ...
             synchronized(right){
              ...
             }
         }
     }

     2️⃣ 动态锁顺序型死锁

    由于方法入参由外部传递而来,方法内部虽然对两个参数按照固定顺序进行加锁,但是由于外部传递时顺序的不可控,而产生锁顺序造成的死锁,即动态锁顺序死锁。

    上例告诉我们,交替的获取锁会导致死锁,且锁是固定的。有时候锁的执行顺序并不那么清晰,参数导致不同的执行顺序。

    经典案例是银行账户转账,from账户向to账户转账,在转账之前先获取两个账户的锁,然后开始转账,如果这是to账户向from账户转账,角色互换,也会导致锁顺序死锁。

    /**
    *动态顺序型死锁
    *转账业务
    */
    publicclassTransferMoneyDeadlock {

      publicstaticvoidtransfer(Accountfrom,Account to,intamount){
          //先锁住转账的账户
          synchronized(from){
              System.out.println("线程【"+Thread.currentThread().getId()+"】获取【"+from.name+"】账户锁成功");
              //休眠增加死锁产生的概率
              sleep(100);
              //在锁住目标账户
              synchronized(to){
                  System.out.println("线程【"+Thread.currentThread().getId()+"】获取【"+to.name+"】账户锁成功");
                  if(from.balance<amount){
                      System.out.println("余额不足");
                      return;
                  }else{
                      from.debit(amount);
                      to.credit(amount);
                      System.out.println("线程【"+Thread.currentThread().getId()+"】从【"+from.name+"】账户转账到【"+to.name+"】账户【"+amount +"】元钱成功");
                  }
              }
          }
      }

      privatestaticclassAccount {
          Stringname;
          intbalance;

          publicAccount(Stringname,intbalance){
              this.name=name;
              this.balance=balance;
          }

          voiddebit(intamount){
              this.balance=balance -amount;
          }

          voidcredit(intamount){
              this.balance=balance +amount;
          }
      }


      /**
       *休眠
       *
       * @paramtime
       */
      privatestaticvoidsleep(longtime){
          try{
              Thread.sleep(time);
          }catch(InterruptedExceptione){
              e.printStackTrace();
          }
      }

      publicstaticvoidmain(String[]args){
          //创建线程池
          ExecutorServiceexecutorService =Executors.newFixedThreadPool(10);
          //创建账户A
          Account A =newAccount("A",100);
          //创建账户B
          Account B =newAccount("B",200);
          //A-> B 的转账
          executorService.execute(()->transfer(A,B,5));
          //B-> A 的转账
          executorService.execute(()->transfer(B,A,10));
          executorService.shutdown();
      }
    }

    输出:

    线程【12】获取【A】账户锁成功
    线程【13】获取【B】账户锁成功

    然后就没有然后了,产生了死锁,我们发现因为对象的调用关系,产生了互相锁住资源的问题。

      💡 解决方案

    根据传入对象的hashCode硬性确定加锁顺序,消除可变性,避免死锁。

    packagecom.test.thread.deadlock;

    importjava.util.concurrent.ExecutorService;
    importjava.util.concurrent.Executors;

    /**
    *动态顺序型死锁解决方案
    */
    publicclassTransferMoneyDeadlock {
      /**
       *监视器,第三把锁,为了方式HASH冲突
       */
      privatestaticObjectlock =newObject();

      /**
       *我们经过上一次得失败,明白了不能依赖参数名称简单的确定锁的顺序,因为参数是
       *具有动态性的,所以,我们改变一下思路,直接根据传入对象的hashCode()大小来
       *对锁定顺序进行排序(这里要明白的是如何排序不是关键,有序才是关键)
       *
       * @paramfrom
       *@paramto
       *@paramamount
       */
      publicstaticvoidtransfer(Accountfrom,Account to,intamount){
          /**
           *这里需要说明一下为什么不使用HashCode()因为HashCode方法可以被重写,
           *所以,我们无法简单的使用父类或者当前类提供的简单的hashCode()方法,
           *所以,我们就使用系统提供的identityHashCode()方法,该方法保证无论
           *你是否重写了hashCode方法,都会在虚拟机层面上调用一个名为JVM_IHashCode
           *的方法来根据对象的存储地址来获取该对象的hashCode(),HashCode如果不重写
           *的话,其实也是通过这个虚拟机层面上的方法,JVM_IHashCode()方法实现的
           *这个方法是用C++实现的。
           */
          intfromHash =System.identityHashCode(from);
          inttoHash =System.identityHashCode(to);
          if(fromHash>toHash){
              //先锁住转账的账户
              synchronized(from){
                  System.out.println("线程【"+Thread.currentThread().getId()+"】获取【"+from.name+"】账户锁成功");
                  //休眠增加死锁产生的概率
                  sleep(100);
                  //在锁住目标账户
                  synchronized(to){
                      System.out.println("线程【"+Thread.currentThread().getId()+"】获取【"+to.name+"】账户锁成功");
                      if(from.balance<amount){
                          System.out.println("余额不足");
                          return;
                      }else{
                          from.debit(amount);
                          to.credit(amount);
                          System.out.println("线程【"+Thread.currentThread().getId()+"】从【"+from.name+"】账户转账到【"+to.name+"】账户【"+amount +"】元钱成功");
                      }
                  }
              }
          }elseif(fromHash<toHash){
              //先锁住转账的账户
              synchronized(to){
                  System.out.println("线程【"+Thread.currentThread().getId()+"】获取【"+from.name+"】账户锁成功");
                  //休眠增加死锁产生的概率
                  sleep(100);
                  //在锁住目标账户
                  synchronized(from){
                      System.out.println("线程【"+Thread.currentThread().getId()+"】获取【"+to.name+"】账户锁成功");
                      if(from.balance<amount){
                          System.out.println("余额不足");
                          return;
                      }else{
                          from.debit(amount);
                          to.credit(amount);
                          System.out.println("线程【"+Thread.currentThread().getId()+"】从【"+from.name+"】账户转账到【"+to.name+"】账户【"+amount +"】元钱成功");
                      }
                  }
              }
          }else{
              //如果传入对象的Hash值相同,那就加让加第三层锁
              synchronized(lock){
                  //先锁住转账的账户
                  synchronized(from){
                      System.out.println("线程【"+Thread.currentThread().getId()+"】获取【"+from.name+"】账户锁成功");
                      //休眠增加死锁产生的概率
                      sleep(100);
                      //在锁住目标账户
                      synchronized(to){
                          System.out.println("线程【"+Thread.currentThread().getId()+"】获取【"+to.name+"】账户锁成功");
                          if(from.balance<amount){
                              System.out.println("余额不足");
                              return;
                          }else{
                              from.debit(amount);
                              to.credit(amount);
                              System.out.println("线程【"+Thread.currentThread().getId()+"】从【"+from.name+"】账户转账到【"+to.name+"】账户【"+amount +"】元钱成功");
                          }
                      }
                  }
              }
          }

      }

      privatestaticclassAccount {
          Stringname;
          intbalance;

          publicAccount(Stringname,intbalance){
              this.name=name;
              this.balance=balance;
          }

          voiddebit(intamount){
              this.balance=balance -amount;
          }

          voidcredit(intamount){
              this.balance=balance +amount;
          }
      }


      /**
       *休眠
       *
       * @paramtime
       */
      privatestaticvoidsleep(longtime){
          try{
              Thread.sleep(time);
          }catch(InterruptedExceptione){
              e.printStackTrace();
          }
      }

      publicstaticvoidmain(String[]args){
          //创建线程池
          ExecutorServiceexecutorService =Executors.newFixedThreadPool(10);
          //创建账户A
          Account A =newAccount("A",100);
          //创建账户B
          Account B =newAccount("B",200);
          //A-> B 的转账
          executorService.execute(()->transfer(A,B,5));
          //B-> A 的转账
          executorService.execute(()->transfer(B,A,10));
          executorService.shutdown();
      }
    }

    输出

    线程【12】获取【A】账户锁成功
    线程【12】获取【B】账户锁成功
    线程【12】从【A】账户转账到【B】账户【5】元钱成功
    线程【13】获取【B】账户锁成功
    线程【13】获取【A】账户锁成功
    线程【13】从【B】账户转账到【A】账户【10】元钱成功

     3️⃣ 协作对象间的死锁

    在协作对象之间可能存在多个锁获取的情况,但是这些获取多个锁的操作并不像在LeftRightDeadLock或transferMoney中那么明显,这两个锁并不一定必须在同一个方法中被获取。

    如果在持有锁时调用某个外部方法,那么这就需要警惕死锁问题,因为在这个外部方法中可能会获取其他锁,或者阻塞时间过长,导致其他线程无法及时获取当前被持有的锁。

    上述两例中,在同一个方法中获取两个锁。实际上,锁并不一定在同一方法中被获取。经典案例,如出租车调度系统

    /**
    *协作对象间的死锁
    */
    publicclassCoordinateDeadlock {
      /**
       *Taxi 
       */
      staticclassTaxi {
          privateStringlocation;
          privateStringdestination;
          privateDispatcher dispatcher;

          publicTaxi(Dispatcherdispatcher,Stringdestination){
              this.dispatcher=dispatcher;
              this.destination=destination;
          }

          publicsynchronizedStringgetLocation(){
              returnthis.location;
          }

          /**
           *该方法先获取Taxithis对象锁后,然后调用Dispatcher类的方法时,又需要获取
           *Dispatcher类的this方法。
           *
           * @paramlocation
           */
          publicsynchronizedvoidsetLocation(Stringlocation){
              this.location=location;
              System.out.println(Thread.currentThread().getName()+"taxi set location:"+location);
              if(this.location.equals(destination)){
                  dispatcher.notifyAvailable(this);
              }
          }
      }

      /**
       *调度类
       */
      staticclassDispatcher {
          privateSet<Taxi>taxis;
          privateSet<Taxi>availableTaxis;

          publicDispatcher(){
              taxis =newHashSet<Taxi>();
              availableTaxis =newHashSet<Taxi>();
          }

          publicsynchronizedvoidnotifyAvailable(Taxitaxi){
              System.out.println(Thread.currentThread().getName()+"notifyAvailable.");
              availableTaxis.add(taxi);
          }

          /**
           *打印当前位置:有死锁风险
           *持有当前锁的时候,同时调用TaxigetLocation这个外部方法;而这个外部方法也是需要加锁的
           *reportLocation的锁的顺序与TaxisetLocation锁的顺序完全相反
           */
          publicsynchronizedvoidreportLocation(){
              System.out.println(Thread.currentThread().getName()+"report location.");
              for(Taxit :taxis){
                  t.getLocation();
              }
          }

          publicvoidaddTaxi(Taxitaxi){
              taxis.add(taxi);
          }
      }

      publicstaticvoidmain(String[]args){
          ExecutorServiceexecutorService =Executors.newFixedThreadPool(10);
          finalDispatcher dispatcher =newDispatcher();
          finalTaxi taxi =newTaxi(dispatcher,"软件园");
          dispatcher.addTaxi(taxi);
          //先获取dispatcher锁,然后是taxi的锁
          executorService.execute(()->dispatcher.reportLocation());
          //先获取taxi锁,然后是dispatcher的锁
          executorService.execute(()->taxi.setLocation("软件园"));
          executorService.shutdown();
      }
    }

      💡 解决方案

    使用开放调用,开放调用指调用该方法不需要持有锁。

    开放调用,是指在调用某个方法时不需要持有锁。开放调用可以避免死锁,这种代码更容易编写。上述调度算法完全可以修改为开发调用,修改同步代码块的范围,使其仅用于保护那些涉及共享状态的操作,避免在同步代码块中执行方法调用。

    修改Dispatcher的reportLocation方法:

    setLocation方法

    /**
      *开放调用,不持有锁期间进行外部方法调用
      *
      * @paramlocation
      */
     publicvoidsetLocation(Stringlocation){
         synchronized(this){
             this.location=location;
         }
         System.out.println(Thread.currentThread().getName()+"taxi set location:"+location);
         if(this.location.equals(destination)){
             dispatcher.notifyAvailable(this);
         }
     }

    reportLocation 方法

    /**
         *同步块只包含对共享状态的操作代码
         */
        publicsynchronizedvoidreportLocation(){
            System.out.println(Thread.currentThread().getName()+"report location.");
            Set<Taxi>taxisCopy;
            synchronized(this){
                taxisCopy =newHashSet<Taxi>(taxis);
            }
            for(Taxit :taxisCopy){
                t.getLocation();
            }
        }

    死锁是一个很有意思的话题,实际工作中接触到的频率还蛮高的。

    作为开发人员吗,我们也尽量避免死锁的出现,以上三种经典案例及解决方法呢,或许可以帮助到大家。

  • 相关阅读:
    使用CSS的Positions布局打造响应式网页
    NoSQL技术——Redis
    C语言函数调用栈
    d区间函数属性实践
    R语言七天入门教程五:认识并使用函数
    不同架构的详细分析VIP架构
    RK3588 rtc-hym8563设备开发
    OpenHD改造实现廉价高清数字图传(树莓派zero + ubuntu PC )——(一)概述
    化工行业数字化供应链系统:赋能化工企业高质量发展,促进上下游协同
    Ubuntu18.04 redis 哨兵模式搭建的步骤
  • 原文地址:https://blog.csdn.net/JACK_SUJAVA/article/details/125430407