使用 java.util.concurrent.locks.Lock 而不是 Synchronized:我的代码可以避免银行转帐场景中的死锁吗?

Using java.util.concurrent.locks.Lock instead of Synchronized: can my code avoid dead-lock in a bank transfer scenario?

我正在研究避免死锁的措施,其中一种可能的方法是通过强制线程放弃它在访问另一个锁但锁不可访问时放弃它已经持有的锁来打破循环等待.

以最简单的银行转账为例:


class Account {
  private int balance;
 
  void transfer(Account target, int amt){
    //lock the from account
    synchronized(this) {              
      //lock the to account
      synchronized(target) {           
        if (this.balance > amt) {
          this.balance -= amt;
          target.balance += amt;
        }
      }
    }
  } 
}

使用嵌套synchronized块时,当一个线程持有当前账户的监视器时,当它无法访问目标账户的锁时,它将被阻塞直到锁可用,并且无法响应到任何中断。它没有机会先尝试第二个锁是否可用,这样就没有机会放弃它已经持有的锁。 而Lock接口提供了tryLock()方法,可以无缝支持场景 .它还提供了 lockInterruptibly() ,这意味着当它由于第二个锁无法访问而被阻塞时,它仍然可以响应中断。但我想知道的是,当被中断时,是什么确保线程会丢弃它已经持有的所有锁?因为我没有在任何地方找到像 Object.wait() 这样的文档,它有文档明确说明它将放弃它持有的所有同步。除了 Thread.interrupt() 只是调用一个本地方法,我看不到任何实现。 好吧,我可以凭直觉理解必须释放锁,否则在线程被中断后,其他线程将无法访问锁,但我只想知道我自己到底发生了什么。


更新: 正如@Holger 指出的那样,我在理解无法访问监视器时 Object.wait() 所做的事情时犯了一个错误。并且我也尝试过针对这个场景做一个锁的解决方案,但是不知道是不是有什么谬误。如果代码中有错误,希望有人能指出。 而且我还想知道我的代码是否可以处理可能提出的方案。 下面是代码:

    class Account {
        private int balance;
        Lock lock = new ReentrantLock();

        void transfer(Account target, int amt) {
            //lock the from account
            try {
                if (lock.tryLock(50, TimeUnit.MILLISECONDS)) {
                    if (target.lock.tryLock(50, TimeUnit.MILLISECONDS)) {
                        this.balance -= amt;
                        target.balance += amt;
                        target.lock.unlock();
                    }
                    lock.unlock();
                }

            } catch (InterruptedException e) {
                System.out.println("Interrupted while attempting the lock.");
            }
        }
    }

此外,如果有人可以提供有关如何使用 lockInterruptibly() 处理这种情况的示例,我将不胜感激。

您使用 tryLock 的方法正在朝着无死锁解决方案的方向发展。但是您必须确保在特殊情况下正确关闭锁。此外,由于 tryLock 可能会失败,导致操作未执行,因此您需要 return 状态:

class Account {
    private int balance;
    Lock lock = new ReentrantLock();

    boolean transfer(Account target, int amt) {
        boolean success = false;
        //lock the from account
        try {
            if(lock.tryLock(50, TimeUnit.MILLISECONDS)) try {
                if(target.lock.tryLock(50, TimeUnit.MILLISECONDS)) try {
                    this.balance -= amt;
                    target.balance += amt;
                    success = true;
                }
                finally {
                    target.lock.unlock();
                }
            }
            finally {
                lock.unlock();
            }
        } catch(InterruptedException ex) {
            // success still false
        }
        return success;
    }
}

然后,你就得考虑怎么处理失败了,重试还是放弃。这导致了另一个问题,即所谓的活锁问题。当一个线程试图从 A 传输到 B 而另一个线程试图从 B 传输到 A 时,两者都可能成功地锁定了它们的源,然后未能锁定另一个。重复尝试可能会反复导致失败。涉及更多线程的类似场景是可能的。在这些场景中,没有发生死锁,但线程仍然无法取得进展。

该解决方案类似于另一种避免死锁的方法:始终按预定义的顺序获取锁。

class Account {
    final BigInteger accountNumber;
    private int balance;
    Lock lock = new ReentrantLock();

    Account(BigInteger accountNumber) {
        this.accountNumber = accountNumber;
    }

    void transfer(Account target, int amt) {
        if(accountNumber.compareTo(target.accountNumber) < 0)
            transfer(this, target, amt);
        else
            transfer(target, this, -amt);
    }

    static void transfer(Account from, Account to, int amt) {
        from.lock.lock();
        try {
            to.lock.lock();
            try {
                from.balance -= amt;
                to.balance += amt;
            }
            finally {
                to.lock.unlock();
            }
        }
        finally {
            from.lock.unlock();
        }
    }
}

通过始终先锁定数字较小的帐户,我们永远不会遇到另一个拥有较大数字锁的线程试图获取我们已经拥有的锁的情况。