并发主题

线程死锁研究之四

上页

前面我们已经根据死锁现象追踪到代码86的位置:

private void transfer(Account fromAccount, Account toAccount, int transferAmount) throws OverdrawnException {

      synchronized (fromAccount) {
        synchronized (toAccount) {
          fromAccount.withdraw(transferAmount);
          toAccount.deposit(transferAmount);
        }
      }
    }

从下面看出,我们是从账目数组随机选择两个帐户对象作为的fromAccount和toAccount,然后锁定它们

Account toAccount = accounts.get(rnd.nextInt(NUM_ACCOUNTS));
Account fromAccount = accounts.get(rnd.nextInt(NUM_ACCOUNTS));

那么我们只有规定顺序了之后就可以解决这个问题:

private void transfer(Account fromAccount, Account toAccount, int transferAmount) throws OverdrawnException {

     if (fromAccount.getNumber() > toAccount.getNumber()) {

       synchronized (fromAccount) {
         synchronized (toAccount) {
           fromAccount.withdraw(transferAmount);
           toAccount.deposit(transferAmount);
         }
       }
     } else {

       synchronized (toAccount) {
         synchronized (fromAccount) {
           fromAccount.withdraw(transferAmount);
           toAccount.deposit(transferAmount);
         }
       }
     }
   }

其中根据getNumber大小进行转账。使用 account的 number 确保我们锁住的首先总是number最高的那个 Account对象,这样死锁就避免了。

下面是完整fixed的代码:

public class AvoidsDeadlockDemo {

  private static final int NUM_ACCOUNTS = 10;
  private static final int NUM_THREADS = 20;
  private static final int NUM_ITERATIONS = 100000;
  private static final int MAX_COLUMNS = 60;

  static final Random rnd = new Random();

  List<Account> accounts = new ArrayList<Account>();

  public static void main(String args[]) {

    AvoidsDeadlockDemo demo = new AvoidsDeadlockDemo();
    demo.setUp();
    demo.run();
  }

  void setUp() {

    for (int i = 0; i < NUM_ACCOUNTS; i++) {
      Account account = new Account(i, rnd.nextInt(1000));
      accounts.add(account);
    }
  }

  void run() {

    for (int i = 0; i < NUM_THREADS; i++) {
      new BadTransferOperation(i).start();
    }
  }

  class BadTransferOperation extends Thread {

    int threadNum;

    BadTransferOperation(int threadNum) {
      this.threadNum = threadNum;
    }

    @Override
    public void run() {

      for (int i = 0; i < NUM_ITERATIONS; i++) {

        Account toAccount = accounts.get(rnd.nextInt(NUM_ACCOUNTS));
        Account fromAccount = accounts.get(rnd.nextInt(NUM_ACCOUNTS));
        int amount = rnd.nextInt(1000);

        if (!toAccount.equals(fromAccount)) {
          try {
            transfer(fromAccount, toAccount, amount);
            System.out.print(".");
          } catch (OverdrawnException e) {
            System.out.print("-");
          }

          printNewLine(i);
        }
      }
      System.out.println("Thread Complete: " + threadNum);
    }

    private void printNewLine(int columnNumber) {

      if (columnNumber % MAX_COLUMNS == 0) {
        System.out.print("\n");
      }
    }

    /**
     * This is the crucial point here. The idea is that to avoid deadlock you need to ensure that threads can't try
     * to lock the same two accounts in the same order
     */
    private void transfer(Account fromAccount, Account toAccount, int transferAmount) throws OverdrawnException {

      if (fromAccount.getNumber() > toAccount.getNumber()) {

        synchronized (fromAccount) {
          synchronized (toAccount) {
            fromAccount.withdraw(transferAmount);
            toAccount.deposit(transferAmount);
          }
        }
      } else {

        synchronized (toAccount) {
          synchronized (fromAccount) {
            fromAccount.withdraw(transferAmount);
            toAccount.deposit(transferAmount);
          }
        }
      }
    }
  }
}

 

需要注意的是:只要你在同一个线程锁住两个或更多不同对象,顺序就影响着死锁的发生。

 

其他解决办法:显式使用并发锁ReentrantLock。

 

public class Account implements Lock {

  private final int number;

  private int balance;

  private final ReentrantLock lock;

  public Account(int number, int openingBalance) {
    this.number = number;
    this.balance = openingBalance;
    this.lock = new ReentrantLock();
  }

  public void withDrawAmount(int amount) throws OverdrawnException {

    if (amount > balance) {
      throw new OverdrawnException();
    }

    balance -= amount;
  }

  public void deposit(int amount) {

    balance += amount;
  }

  public int getNumber() {
    return number;
  }

  public int getBalance() {
    return balance;
  }

  // ------- Lock interface implementation

  @Override
  public void lock() {
    lock.lock();
  }

  @Override
  public void lockInterruptibly() throws InterruptedException {
    lock.lockInterruptibly();
  }

  @Override
  public Condition newCondition() {
    return lock.newCondition();
  }

  @Override
  public boolean tryLock() {
    return lock.tryLock();
  }

  @Override
  public boolean tryLock(long arg0, TimeUnit arg1) throws InterruptedException {
    return lock.tryLock(arg0, arg1);
  }

  @Override
  public void unlock() {
    if (lock.isHeldByCurrentThread()) {
      lock.unlock();
    }
  }

}

注意小技巧:解锁:

@Override
  public void unlock() {
    if (lock.isHeldByCurrentThread()) {
      lock.unlock();
    }
  }

这段检查调用线程是否是当前拥有锁的线程。如果错过了这行代码,你会得到以下IllegalMonitorStateException:

Exception in thread 'Thread-7' java.lang.IllegalMonitorStateException
at java.util.concurrent.locks.ReentrantLock$Sync.tryRelease(ReentrantLock.java:155)
at java.util.concurrent.locks.AbstractQueuedSynchronizer.release(AbstractQueuedSynchronizer.java:1260)
at java.util.concurrent.locks.ReentrantLock.unlock(ReentrantLock.java:460)
at threads.lock.Account.unlock(Account.java:76)
at threads.lock.TrylockDemo$BadTransferOperation.transfer(TrylockDemo.java:98)
at threads.lock.TrylockDemo$BadTransferOperation.run(TrylockDemo.java:67)

下面看看这个新的Account如何调用:

public class TrylockDemo {

  private static final int NUM_ACCOUNTS = 10;
  private static final int NUM_THREADS = 20;
  private static final int NUM_ITERATIONS = 100000;
  private static final int LOCK_ATTEMPTS = 10000;

  static final Random rnd = new Random();

  List<Account> accounts = new ArrayList<Account>();

  public static void main(String args[]) {

    TrylockDemo demo = new TrylockDemo();
    demo.setUp();
    demo.run();
  }

  void setUp() {

    for (int i = 0; i < NUM_ACCOUNTS; i++) {
      Account account = new Account(i, 1000);
      accounts.add(account);
    }
  }

  void run() {

    for (int i = 0; i < NUM_THREADS; i++) {
      new BadTransferOperation(i).start();
    }
  }

  class BadTransferOperation extends Thread {

    int threadNum;

    BadTransferOperation(int threadNum) {
      this.threadNum = threadNum;
    }

    @Override
    public void run() {

      int transactionCount = 0;

      for (int i = 0; i < NUM_ITERATIONS; i++) {

        Account toAccount = accounts.get(rnd.nextInt(NUM_ACCOUNTS));
        Account fromAccount = accounts.get(rnd.nextInt(NUM_ACCOUNTS));
        int amount = rnd.nextInt(1000);

        if (!toAccount.equals(fromAccount)) {

          boolean successfulTransfer = false;

          try {
            successfulTransfer = transfer(fromAccount, toAccount, amount);

          } catch (OverdrawnException e) {
            successfulTransfer = true;
          }

          if (successfulTransfer) {
            transactionCount++;
          }

        }
      }

      System.out.println("Thread Complete: " + threadNum + " Successfully made " + transactionCount + " out of "
          + NUM_ITERATIONS);
    }

    private boolean transfer(Account fromAccount, Account toAccount, int transferAmount) throws OverdrawnException {

      boolean success = false;
      for (int i = 0; i < LOCK_ATTEMPTS; i++) {

        try {
          if (fromAccount.tryLock()) {
            try {
              if (toAccount.tryLock()) {

                success = true;
                fromAccount.withDrawAmount(transferAmount);
                toAccount.deposit(transferAmount);
                break;
              }
            } finally {
              toAccount.unlock();
            }
          }
        } finally {
          fromAccount.unlock();
        }
      }

      return success;
    }

  }
}

尝试锁定先是从fromAccount然后到toAccount。如果这样正常,在解锁两个帐户之前进行转让。如果帐户已被锁定,那么我的tryLock()方法失败,整个事情的循环下去,再次尝试。 10000锁定尝试后,线程放弃,而忽略转让。我想,在现实世界中的应用,你想要把这个失败放到到某种形式的队列,以便它可以稍后被调出进行调查。

如下输出结果:

Thread Complete: 17 Successfully made 58142 out of 100000
Thread Complete: 12 Successfully made 57627 out of 100000
Thread Complete: 9 Successfully made 57901 out of 100000
Thread Complete: 16 Successfully made 56754 out of 100000
Thread Complete: 3 Successfully made 56914 out of 100000
Thread Complete: 14 Successfully made 57048 out of 100000
Thread Complete: 8 Successfully made 56817 out of 100000
Thread Complete: 4 Successfully made 57134 out of 100000
Thread Complete: 15 Successfully made 56636 out of 100000
Thread Complete: 19 Successfully made 56399 out of 100000
Thread Complete: 2 Successfully made 56603 out of 100000
Thread Complete: 13 Successfully made 56889 out of 100000
Thread Complete: 0 Successfully made 56904 out of 100000
Thread Complete: 5 Successfully made 57119 out of 100000
Thread Complete: 7 Successfully made 56776 out of 100000
Thread Complete: 6 Successfully made 57076 out of 100000
Thread Complete: 10 Successfully made 56871 out of 100000
Thread Complete: 11 Successfully made 56863 out of 100000
Thread Complete: 18 Successfully made 56916 out of 100000
Thread Complete: 1 Successfully made 57304 out of 100000

下面是第二个版本,使用.tryLock(LOCK_TIMEOUT, TimeUnit.MILLISECONDS)替代循环:

private boolean transfer(Account fromAccount, Account toAccount, int transferAmount) throws OverdrawnException {

      boolean success = false;

      try {
        if (fromAccount.tryLock(LOCK_TIMEOUT, TimeUnit.MILLISECONDS)) {
          try {
            if (toAccount.tryLock(LOCK_TIMEOUT, TimeUnit.MILLISECONDS)) {

              success = true;
              fromAccount.withDrawAmount(transferAmount);
              toAccount.deposit(transferAmount);
            }
          } finally {
            toAccount.unlock();
          }
        }
      } catch (InterruptedException e) {
        e.printStackTrace();
      } finally {
        fromAccount.unlock();
      }

      return success;
    }

 

输出:

Thread Complete: 0 Successfully made 26637 out of 100000
Thread Complete: 14 Successfully made 26516 out of 100000
Thread Complete: 3 Successfully made 26552 out of 100000
Thread Complete: 11 Successfully made 26653 out of 100000
Thread Complete: 7 Successfully made 26399 out of 100000
Thread Complete: 1 Successfully made 26602 out of 100000
Thread Complete: 18 Successfully made 26606 out of 100000
Thread Complete: 17 Successfully made 26358 out of 100000
Thread Complete: 19 Successfully made 26407 out of 100000
Thread Complete: 16 Successfully made 26312 out of 100000
Thread Complete: 15 Successfully made 26449 out of 100000
Thread Complete: 5 Successfully made 26388 out of 100000
Thread Complete: 8 Successfully made 26613 out of 100000
Thread Complete: 2 Successfully made 26504 out of 100000
Thread Complete: 6 Successfully made 26420 out of 100000
Thread Complete: 4 Successfully made 26452 out of 100000
Thread Complete: 9 Successfully made 26287 out of 100000
Thread Complete: 12 Successfully made 26507 out of 100000
Thread Complete: 10 Successfully made 26660 out of 100000
Thread Complete: 13 Successfully made 26523 out of 100000

上面的结果表明,使用一个计时器时,余额转移的成功率下降略多于25%。虽然它现在不耗费堆栈时间,它仍然是非常低效的。

 

 

java多线程

Java同步或锁

Java性能调优