【问题标题】:Why should we use condition and what's the differrence between Blocked and Waiting为什么要使用条件,阻塞和等待有什么区别
【发布时间】:2016-12-27 06:21:39
【问题描述】:

“Core Java”一书中有一个例子,它将资金从一个账户转移到另一个账户。不知道条件有什么用?在书中,它告诉我们:

如果我们只是无条件地锁定和等待,就会陷入死锁:

private final double[] accounts;
private Lock bankLock;
private Condition sufficientFunds;

public void transfer(int from, int to, int amount) {
    bankLock.lock();
    try { 
        while (accounts[from] < amount) {
            // wait...
        }
        // transfer funds . . . 
    } finally { 
        bankLock.unlock(); 
    }
}

现在,当账户里没有足够的钱时我们该怎么办?我们 等到其他线程添加了资金。但是这个线程刚刚 获得了对bankLock 的独占访问权,因此没有其他线程拥有 有机会存款。

通话时

sufficientFunds.await();

当前线程现在已停用并放弃锁定。这让 在另一个线程中,我们希望可以增加帐户余额。

Lock锁码,条件放弃锁,但我不知道条件是什么,为什么不在钱不够的时候简单地解锁呢? 线程状态 Blocked 和 Waiting 有什么区别? block:线程无法运行; 等待:线程也不能运行。 有什么不同?

另一个问题:

while (accounts[from] < amount) {
   ...
   sufficientFunds.await();

为什么不写if

if (accounts[from] < amount) {
    ...

【问题讨论】:

  • 一个问题只问一个问题。如果您有两个问题,请创建两个问题。如果您在一个问题中有多个问题,则无法确定什么是好的答案。

标签: java multithreading locking conditional-statements


【解决方案1】:

我认为在这种情况下是由synchronized 的例子采取的,并改变了(对于最新的书籍修订,但当然不是没有错误)Lock 的使用。对于带有synchronized 的此类代码,没有问题:

   private final double[] accounts;
   private Condition sufficientFunds;
    public synchronized void transfer(int from, int to, int amount)
    {
         try { 
            while (accounts[from] < amount) {
            // wait . . . } 
        // transfer funds . . . 
         } finally {
            this.notifyAll();
         }
    }

当我们调用这个方法时,我们获得了一个锁,但是当我们调用wait() 时,我们释放它并等待notifyAll() 方法被调用或指定的时间到了。醒来后,我们再次尝试获取锁并再次检查我们的状态......并重复这一系列操作,直到我们获得足够的钱。


这张图向您展示了 WAITING 和 BLOCKED 之间的区别:


这里我们使用

while (accounts[from] < amount) {

而不是

if (accounts[from] < amount) {

因为我们希望获得方法调用的积极结果,但我们不能保证另一个线程会将所有丢失的钱都交到我们预期的帐户中。 例如,如果你是一家银行,你打电话给

减少(约翰,1000)

但是客户 John 没有足够的钱(500 美元而不是 1000 美元),如果您的 reduce 操作包含在 if 而不是 while 中,您将只尝试等待一次。你不走运,此时银行里没有客户。所以你进入if-block 等待某个时间并再次获得锁定,但你账户上的金额没有改变(仍然是 500 美元)。您不执行重复检查(因为 if-block 而不是 while),而是进行传输操作,这几乎不是我们真正想要的。


方法sufficientFunds.await();必须在其中进行三个操作:releaseLock()、wait()、lock()。这意味着我们释放锁以允许另一个线程与银行合作并等待它完成操作,然后我们获取锁以再次检查我们的余额并计划transfer(),如果我们现在获得足够的钱。

【讨论】:

    【解决方案2】:

    首先是while块的原因

    其中的代码只是在该行等待线程。编写循环只是为了在将来通知线程时(通过任何机制,例如添加资金并在此之后使用 notifyAll 的线程),它会再次检查 while 循环中给出的条件。如果金额仍然大于账户,它将再次进入等待状态并离开锁定。 这将允许其他线程添加资金,然后再次让上述线程关注 while 条件。 一旦while条件失败,它不会等待,将继续转移资金。因此,将通过解锁条件离开锁。

    阻塞和等待的区别:

    如果我们专注于正确使用等待,我们可以避免被称为阻塞的情况。当我们等待时,我们认为代码的编写方式是线程将正确地相互通知而不会造成死锁。 如果 thread1 正在等待依赖于 thread2 的条件,而 thread2 正在等待依赖于 thread1 的条件,则会发生死锁或阻塞。

    【讨论】:

      【解决方案3】:

      Bank 类有一个特殊要求:如果要求它从一个账户向另一个账户转移一笔钱,而源账户上没有足够的钱,它必须等到存入足够的钱才能进行转账可能的。您可以运行一个循环来检查每次迭代是否有足够的钱,并仅在满足此条件时获取锁:

      while (true) {
          if (accounts[from] >= amount) {
              bankLock.lock();
              try {
                  if (account[from] >= amount) {
                      // do the transfer here...
                      break;
                  }
              } finally {
                  bankLock.unlock();
              }
          }
      }
      

      但是这种方法:

      • 在不断的检查中浪费 CPU 资源(它没有人 在几小时或几天内存入足够的钱?)
      • 看起来笨重且不习惯
      • 并不总是有效(原因不在本问题的范围内,如果您有兴趣,我可以提供 cmets 中解释的链接)

      因此,您需要一些机制来告知您只是在等待帐户中的某些更改。如果没有人将钱存入其中,则一次又一次地检查帐户中的金额是很浪费的。还有更多 – 您还需要在有人存入资金后立即获得锁定,这样您就可以专门检查新帐户状态并决定是否可以进行转账。

      您还必须记住,存款并不是账户上唯一允许的操作。例如,还有提款。如果有人提款,则无需检查您的账户是否有可能转帐,因为我们确信现在的钱更少了。因此,我们希望在存款时被唤醒,但不希望在提款时被唤醒。我们需要以某种方式将它们分开。这就是条件发挥作用的地方。

      Condition 是一个实现Condition 接口的对象。它只是一种抽象,允许您将锁定/等待逻辑划分为多个部分。在我们的例子中,我们可能有两个条件:一个用于增加账户余额,另一个用于减少(例如,如果有人正在等待将银行账户归零以关闭它):

      sufficientFunds = bankLock.newCondition();
      decreasedFunds = bankLock.newCondition();
      

      现在,您无需在循环中进行大量检查,您可以组织您的程序,让您只有在有人为帐户注资时才醒来并检查帐户:

      private final double[] accounts;
      private Lock bankLock;
      private Condition sufficientFunds;
      
      public void transfer(int from, int to, int amount) {
          bankLock.lock();
          try { 
              while (accounts[from] < amount) {
                  sufficientFunds.await();
              }
              // transfer funds ...
              sufficientFunds.signalAll();
          } finally { 
              bankLock.unlock(); 
          }
      }
      
      public void deposit(int to, int amount) {
          bankLock.lock();
          try {
              // deposit funds...
              sufficientFunds.signalAll();
          } finally {
              bankLock.unlock();
          }
      }
      

      那么,让我们看看这里发生了什么并回答您的问题:

      1. transfer() 正在尝试获取bankLock。如果有人已经持有这个锁,你的线程就会被另一个线程阻塞,直到锁被释放。

      2. 当另一个线程释放锁时,您获取它并可以检查帐户的状态。如果钱不够,你决定坐等有人把钱存入账户,所以你打电话给sufficienFunds.await()。它使您的线程等待某事发生。你决定这样做,不仅仅是因为另一个线程而被阻塞,这就是 blockedwaiting 之间的区别。但是你是对的,在这两种情况下你的线程都没有运行,所以区别更符合逻辑,而不是技术。

      3. bankLock 上创建的条件上调用await() 会释放此锁并使其可供其他线程获取和执行操作。如果不释放锁,您的线程将阻塞银行中的所有操作并导致死锁。

      4. 现在,当有人对帐户进行任何更改以增加金额时,它会通知sufficientFunds 条件,我们的传输线程唤醒并且循环可以进行另一次检查。在这里,我们有两件事要看。首先,当我们的线程唤醒时,它会自动重新获取锁,因此我们可以确保我们可以安全地排他地进行检查和修改。其次,可能会出现新的金额仍然不足以进行转移(在条件上发出信号仅表示发生了某些事情,可能会改变您正在等待的状态,但不能保证该状态)。在这种情况下,我们必须等待下一次存款。这就是为什么我们必须使用while 循环,而不是简单的if

      5. 当账户里的钱终于足够时,我们进行转账。我们在转账后也会打电话给sufficienFunds.signalAll(),因为我们增加了to账户中的金额。如果其他线程正在等待该帐户的资金,它会获取锁并可以使其工作。

      因此,使用锁和条件可以让您以安全有效的方式组织多线程程序。

      【讨论】:

      • 非常感谢,我想看看链接页面,了解为什么使用锁定而不是条件并不总是有效。 “并不总是有效(原因超出了这个问题的范围,如果您对此感兴趣,我可以提供指向 cmets 解释的链接)”@Andrew Lygin
      • 我的评论是关于我在第一个代码 sn-p 中使用的双重检查模式。在某些情况下它可能会中断:cs.umd.edu/~pugh/java/memoryModel/DoubleCheckedLocking.html
      猜你喜欢
      • 1970-01-01
      • 2017-11-11
      • 1970-01-01
      • 1970-01-01
      • 2019-10-16
      • 1970-01-01
      • 2023-03-28
      • 2012-05-22
      • 1970-01-01
      相关资源
      最近更新 更多