为什么java.util.concurrent.ArrayBlockingQueue使用'while'循环而不是'if'围绕对 await()的调用?

2022-09-03 17:09:07

我一直在玩我自己的版本,使用“if”,一切似乎都很好。当然,如果使用signalAll()而不是signal(),这将严重崩溃,但是如果一次只通知一个线程,这怎么会出错呢?

他们的代码在这里 - 查看 put() 和 take() 方法;一个更简单、更切中要害的实现可以在 JavaDoc for Condition 的顶部看到。

下面是我实现的相关部分。

public Object get() {
    lock.lock();
    try {
        if( items.size() < 1 )
            hasItems.await();
        Object poppedValue = items.getLast();
        items.removeLast();
        hasSpace.signal();
        return poppedValue; 
    } catch (InterruptedException e) {
        e.printStackTrace();
        return null;
    } finally {
        lock.unlock();
    }
}

public void put(Object item) {
    lock.lock();
    try {
        if( items.size() >= capacity )
            hasSpace.await();
        items.addFirst(item);
        hasItems.signal();
        return;
    } catch (InterruptedException e) {
        e.printStackTrace();
    } finally {
        lock.unlock();
    }
}

附言:我知道一般来说,特别是在像这样的lib类中,人们应该让异常渗透进来。


答案 1

防止虚假唤醒。JVM 无法向您保证线程将再次开始运行的唯一可能原因是您以预期的方式调用了 signal。有时它会不小心开始并离开(虚假醒来)。因此,如果您要运行的条件实际上不成立,则必须再次等待。

这在 javadoc 中对等待方法进行了解释:http://java.sun.com/javase/6/docs/api/java/lang/Object.html#wait%28long%29

并在等待的文档中提到:http://java.sun.com/javase/6/docs/api/java/util/concurrent/locks/Condition.html#await%28%29

与此条件关联的锁以原子方式释放,并且当前线程因线程调度目的而被禁用并处于休眠状态,直到发生以下四种情况之一:

  • 其他一些线程调用此条件的signal()方法,并且当前线程恰好被选为要唤醒的线程;或

  • 其他一些线程调用此条件的 signalAll() 方法;或

  • 其他一些线程中断当前线程,并且支持中断线程悬架;或

* 发生“虚假唤醒”。

Condition 接口的某些实现可能会抑制虚假唤醒,但依赖于此实现将依赖于实现细节,并使代码无法移植。


答案 2

为什么java.util.concurrent.ArrayBlockingQueue使用'while'循环而不是'if'围绕对 await()的调用?

它们用于防止生产者/消费者模型中的经典线程争用条件,并防止更罕见的虚假唤醒情况。whileif

当(例如)多个使用者正在等待特定条件(如队列为空)并且该条件收到通知时,另一个线程可能会首先锁定并“窃取”添加到队列中的项目。线程必须使用循环来确保队列在尝试取消排队之前具有项目。while

示例代码

我编写了一些示例代码和更多文档来演示竞争条件。

竞态条件的说明

查看您的特定代码,比赛如下:

  1. 线程 #1,一个使用者,在 while 循环中,等待队列中有项目await()
  2. 线程 #2,创建器,锁定队列
  3. 线程 #3,一个使用者,完成使用最后一个项目,调用 ,锁定队列,并且必须等待 #2 解锁(它不是在等待,而是在等待获取get()hasItemslock)
  4. 线程 #2,将一个项目添加到队列中,并调用以通知某人那里有一个项目hasItems.signal()
  5. 线程 #1 被唤醒并去锁定队列,必须等待 #2 解锁
  6. 线程 #2 解锁
  7. 线程 #3 位于线程 #1 等待锁定之前,因此它首先锁定队列,进入 while 循环并取消 #1 通知的项目的队列,然后解锁
  8. 线程 #1 现在能够锁定。如果它只是一个语句,它会继续前进,并试图从一个空列表中排队,这将抛出什么的。ifArrayIndexOutOfBoundsException

该语句之所以必要,是因为要处理这些争用条件。在上面的步骤 8 中,线程 #1 将转回测试,以查看队列中是否有项目,发现没有项目,然后返回等待。whilewhile

这是一个经典的问题,它绊倒了许多重入程序员。例如,O'Reilly pthreads圣经的初始版本有没有while循环的示例代码,必须重新发布。

对于某些线程系统,系统更容易唤醒所有条件,而不是已发出信号的特定条件,因此可能发生“虚假唤醒”。循环也可以防止这种情况。while


推荐