Avoid Deadlock example

前端 未结 6 1997
谎友^
谎友^ 2020-12-13 04:51

I am wondering what are the alternative ways to avoid deadlock in the following example. The following example is a typical bank account transferring deadlock problem. What

相关标签:
6条回答
  • 2020-12-13 05:20

    Sort the accounts. The dead lock is from the ordering of the accounts (a,b vs b,a).

    So try:

     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);
               }
          }
     }
    
    0 讨论(0)
  • 2020-12-13 05:20

    In addition to the solution of lock ordered you can also avoid the deadlock by synchronizing on a private static final lock object before performing any account transfers.

     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);
              }
         }
    

    This solution has the problem that a private static lock restricts the system to performing transfers "sequentially".

    Another one can be if each Account has a 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);
            }
    
     }
    

    Deadlock does not occur in this approach because those locks will never be held indefinitely. If the current object's lock is acquired but the second lock is unavailable, the first lock is released and the thread sleeps for some specified amount of time before attempting to reacquire the lock.

    0 讨论(0)
  • 2020-12-13 05:28

    Here is the solution for the problem stated.

    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();
        }
    
    }
    
    0 讨论(0)
  • 2020-12-13 05:29

    There are three requirements you must satisfy:

    1. Consistently reduce the contents of one account by the specified amount.
    2. Consistently increase the contents of the other account by the specified amount.
    3. If one of the above is successful, the other must also be successful.

    You can achieve 1. and 2. by using Atomics, but you will have to use something other that double as there is no AtomicDouble. AtomicLong would probably be your best bet.

    So you're left with your third requirement - if one succeeds the other must succeed. There is a simple technique that works superbly with atomics and that is using the getAndAdd methods.

    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);
    }
    
    0 讨论(0)
  • 2020-12-13 05:37

    You can also create separate lock for each Account (in Account class) and then before doing transaction acquire both locks. Take a look:

    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;
        }
    

    After get two locks you can do transfer without worrying about safe.

        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);
            }
        }
    
    0 讨论(0)
  • 2020-12-13 05:38

    This is a classic question. I see two possible solutions:

    1. To sort accounts and synchronize at account which has an id lower than another one. This method mentioned in the bible of concurrency Java Concurrency in Practice in chapter 10. In this book authors use system hash code to distinguish the accounts. See java.lang.System#identityHashCode.
    2. The second solution is mentioned by you - yes you can avoid nested synchronized blocks and your code will not lead to deadlock. But in that case the processing might have some problems because if you withdraw money from the first account the second account may be locked for any significant time and probably you will need to put money back to the first account. That's not good and because that nested synchronization and the lock of two accounts is better and more commonly used solution.
    0 讨论(0)
提交回复
热议问题