【问题标题】:Avoid Deadlock example避免死锁示例
【发布时间】:2012-10-30 21:09:59
【问题描述】:

我想知道在以下示例中避免死锁的替代方法是什么。下面的例子是一个典型的银行账户转账死锁问题。在实践中解决它有哪些更好的方法?

class Account {
     double balance;
     int id;
     public Account(int id, double balance){
          this.balance = balance;
          this.id = id;
     }
     void withdraw(double amount){
          balance -= amount;
     } 
     void deposit(double amount){
          balance += amount;
     }
}
class Main{
     public static void main(String [] args){
           final Account a = new Account(1,1000);
           final Account b = new Account(2,300);
           Thread a = new Thread(){
                 public void run(){
                     transfer(a,b,200);
                 }
           };
           Thread b = new Thread(){
                 public void run(){
                     transfer(b,a,300);
                 }
           };
           a.start();
           b.start();
     }
     public static void transfer(Account from, Account to, double amount){
          synchronized(from){
               synchronized(to){
                    from.withdraw(amount);
                    to.deposit(amount);
               }
          }
     }
}
  

我想知道如果我像下面这样在我的转移方法中分离嵌套锁是否会解决死锁问题

 synchronized(from){
      from.withdraw(amount);
 }
 synchronized(to){
      to.deposit(amount);
 }

【问题讨论】:

    标签: java multithreading parallel-processing deadlock


    【解决方案1】:

    除了锁定顺序的解决方案,您还可以通过在执行任何帐户转移之前同步私有静态最终锁定对象来避免死锁。

     class Account{
     double balance;
     int id;
     private static final Object lock = new Object();
      ....
    
    
    
    
     public static void transfer(Account from, Account to, double amount){
              synchronized(lock)
              {
                        from.withdraw(amount);
                        to.deposit(amount);
              }
         }
    

    此解决方案的问题是私有静态锁限制系统“按顺序”执行传输。

    如果每个帐户都有一个 ReentrantLock,则可以是另一个:

    private final Lock lock = new ReentrantLock();
    
    
    
    
    public static void transfer(Account from, Account to, double amount)
    {
           while(true)
            {
              if(from.lock.tryLock()){
                try { 
                    if (to.lock.tryLock()){
                       try{
                           from.withdraw(amount);
                           to.deposit(amount);
                           break;
                       } 
                       finally {
                           to.lock.unlock();
                       }
                    }
               }
               finally {
                    from.lock.unlock();
               }
    
               int n = number.nextInt(1000);
               int TIME = 1000 + n; // 1 second + random delay to prevent livelock
               Thread.sleep(TIME);
            }
    
     }
    

    这种方法不会发生死锁,因为这些锁永远不会被无限期地持有。如果获取了当前对象的锁,但第二个锁不可用,则释放第一个锁,线程在尝试重新获取锁之前休眠一段指定的时间。

    【讨论】:

      【解决方案2】:

      这里是上述问题的解决方案。

      import java.util.Random;
      import java.util.concurrent.locks.Lock;
      import java.util.concurrent.locks.ReentrantLock;
      
      public class FixDeadLock1 {
      
          private class Account {
      
              private final Lock lock = new ReentrantLock();
      
              @SuppressWarnings("unused")
              double balance;
              @SuppressWarnings("unused")
              int id;
      
              public Account(int id, double balance) {
                  this.balance = balance;
                  this.id = id;
              }
      
              void withdraw(double amount) {
                  this.balance -= amount;
              }
      
              void deposit(double amount) {
                  balance += amount;
              }
          }
      
          private class Transfer {
      
              void transfer(Account fromAccount, Account toAccount, double amount) {
                  /*
                   * synchronized (fromAccount) { synchronized (toAccount) {
                   * fromAccount.withdraw(amount); toAccount.deposit(amount); } }
                   */
      
                  if (impendingTransaction(fromAccount, toAccount)) {
                      try {
                          System.out.format("Transaction Begins from:%d to:%d\n",
                                  fromAccount.id, toAccount.id);
                          fromAccount.withdraw(amount);
                          toAccount.deposit(amount);
                      } finally {
                          fromAccount.lock.unlock();
                          toAccount.lock.unlock();
                      }
      
                  } else {
                       System.out.println("Unable to begin transaction");
                  }
      
              }
      
              boolean impendingTransaction(Account fromAccount, Account toAccount) {
      
                  Boolean fromAccountLock = false;
                  Boolean toAccountLock = false;
      
                  try {
                      fromAccountLock = fromAccount.lock.tryLock();
                      toAccountLock = toAccount.lock.tryLock();
                  } finally {
                      if (!(fromAccountLock && toAccountLock)) {
                          if (fromAccountLock) {
                              fromAccount.lock.unlock();
                          }
                          if (toAccountLock) {
                              toAccount.lock.unlock();
                          }
                      }
                  }
      
                  return fromAccountLock && toAccountLock;
              }
      
          }
      
          private class WrapperTransfer implements Runnable {
              private Account fromAccount;
              private Account toAccount;
              private double amount;
      
              public WrapperTransfer(Account fromAccount,Account toAccount,double amount){
                  this.fromAccount = fromAccount;
                  this.toAccount = toAccount; 
                  this.amount = amount;
              }
      
              public void run(){
                  Random random = new Random();
                  try {
                      int n = random.nextInt(1000);
                      int TIME = 1000 + n; // 1 second + random delay to prevent livelock
                      Thread.sleep(TIME);
                  } catch (InterruptedException e) {}
                  new Transfer().transfer(fromAccount, toAccount, amount);
              }
      
          }
      
          public void initiateDeadLockTransfer() {
              Account from = new Account(1, 1000);
              Account to = new Account(2, 300);       
              new Thread(new WrapperTransfer(from,to,200)).start();
              new Thread(new WrapperTransfer(to,from,300)).start();
          }
      
          public static void main(String[] args) {
              new FixDeadLock1().initiateDeadLockTransfer();
          }
      
      }
      

      【讨论】:

        【解决方案3】:

        您还可以为每个帐户(在帐户类中)创建单独的锁,然后在执行事务之前获取两个锁。看看:

        private boolean acquireLocks(Account anotherAccount) {
                boolean fromAccountLock = false;
                boolean toAccountLock = false;
                try {
                    fromAccountLock = getLock().tryLock();
                    toAccountLock = anotherAccount.getLock().tryLock();
                } finally {
                    if (!(fromAccountLock && toAccountLock)) {
                        if (fromAccountLock) {
                            getLock().unlock();
                        }
                        if (toAccountLock) {
                            anotherAccount.getLock().unlock();
                        }
                    }
                }
                return fromAccountLock && toAccountLock;
            }
        

        获得两把锁后,您可以进行转移而无需担心安全问题。

            public static void transfer(Acc from, Acc to, double amount) {
                if (from.acquireLocks(to)) {
                    try {
                        from.withdraw(amount);
                        to.deposit(amount);
                    } finally {
                        from.getLock().unlock();
                        to.getLock().unlock();
                    }
                } else {
                    System.out.println(threadName + " cant get Lock, try again!");
                    // sleep here for random amount of time and try do it again
                    transfer(from, to, amount);
                }
            }
        

        【讨论】:

          【解决方案4】:

          您必须满足三个要求:

          1. 始终将一个帐户的内容减少指定的数量。
          2. 不断将其他帐户的内容增加指定的数量。
          3. 如果上述其中一项成功,则另一项也必须成功。

          您可以通过使用Atomics 实现 1. 和 2.,但您必须使用 double 以外的其他内容,因为没有 AtomicDoubleAtomicLong 可能是你最好的选择。

          所以你剩下第三个要求——如果一个成功,另一个必须成功。有一种简单的技术可以很好地处理原子,那就是使用 getAndAdd 方法。

          class Account {
            AtomicLong balance = new AtomicLong ();
          }
          
          ...
          Long oldDebtor = null;
          Long oldCreditor = null;
          try {
            // Increase one.
            oldDebtor = debtor.balance.getAndAdd(value);
            // Decrease the other.
            oldCreditor = creditor.balance.gtAndAdd(-value);
          } catch (Exception e) {
            // Most likely (but still incredibly unlikely) InterruptedException but theoretically anything.
            // Roll back
            if ( oldDebtor != null ) {
              debtor.getAndAdd(-value);
            }
            if ( oldCreditor != null ) {
              creditor.getAndAdd(value);
            }
            // Re-throw after cleanup.
            throw (e);
          }
          

          【讨论】:

          • 原子不会抛出中断异常。
          • 正确!但我敢打赌Account.credit/debit 会在 OP 解决这个问题时抛出一些东西。
          【解决方案5】:

          这是一个经典的问题。我看到了两种可能的解决方案:

          1. 对帐户进行排序并在 id 低于另一个帐户的帐户上进行同步。 这种方法在并发圣经Java Concurrency in Practice第10章中提到。在本书中作者使用系统哈希码来区分帐户。见java.lang.System#identityHashCode
          2. 您提到了第二种解决方案 - 是的,您可以避免嵌套同步块,并且您的代码不会导致死锁。但在这种情况下,处理可能会出现一些问题,因为如果您从第一个帐户中提取资金,则第二个帐户可能会被锁定很长时间,并且您可能需要将资金放回第一个帐户。这不好,因为嵌套同步和锁定两个帐户是更好且更常用的解决方案。

          【讨论】:

            【解决方案6】:

            对帐户进行排序。死锁来自帐户的顺序(a,b vs b,a)。

            那就试试吧:

             public static void transfer(Account from, Account to, double amount){
                  Account first = from;
                  Account second = to;
                  if (first.compareTo(second) < 0) {
                      // Swap them
                      first = to;
                      second = from;
                  }
                  synchronized(first){
                       synchronized(second){
                            from.withdraw(amount);
                            to.deposit(amount);
                       }
                  }
             }
            

            【讨论】:

            • 这将在处理两个以上的帐户时起作用,对吗?
            • 我可能不太理解这个概念,但是两个账户的余额相同的情况呢?据我了解,它们不会被交换,因此死锁仍然存在。
            • @Piotr:不,在这种情况下,您可以根据帐户的独特性(例如帐号或数据库中的主键等)对帐户进行排序。实际排序无关紧要,只要它是所有参与者的稳定排序(即没有重复,如您所建议的)。
            • @WillHartung 谢谢你,Will,这听起来很有说服力!
            猜你喜欢
            • 2017-12-17
            • 1970-01-01
            • 1970-01-01
            • 2020-10-25
            • 1970-01-01
            • 2013-06-07
            • 2013-07-24
            • 2012-08-14
            • 1970-01-01
            相关资源
            最近更新 更多