为什么java.util.concurrent.ArrayBlockingQueue使用’while’循环而不是’if’来调用await()?
我一直在玩我自己的版本,使用’if’,似乎一切正常。 当然,如果使用signalAll()而不是signal(),这将会崩溃,但如果一次只通知一个线程,那怎么会出错呢?
他们的代码在这里 – 检查put()和take()方法; 可以在JavaDoc for Condition的顶部看到更简单,更多点的实现。
我的实施的相关部分如下。
public Object get() { lock.lock(); try { if( items.size() = capacity ) hasSpace.await(); items.addFirst(item); hasItems.signal(); return; } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); } }
PS我知道,一般来说,特别是在像这样的lib类中,应该让exception渗透。
防止虚假唤醒。 JVM无法保证线程再次开始运行的唯一可能原因是您按照预期的方式调用了信号。 有时它会意外地开始并且去(Spurious wake up)。 因此,如果要运行的条件实际上不正确,则必须再次等待。
这在等待方法的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
与此条件关联的锁被primefaces释放,并且当前线程因线程调度而被禁用,并处于hibernate状态,直到发生以下四种情况之一:
一些其他线程为此Condition调用signal()方法,并且当前线程恰好被选为要被唤醒的线程; 要么
其他一些线程为此Condition调用signalAll()方法; 要么
其他一些线程会中断当前线程,并支持线程挂起中断; 要么
*发生“虚假唤醒”。
Condition接口的某些实现可能会抑制虚假唤醒,但依赖于此将依赖于实现细节并使您的代码不可移植。
为什么java.util.concurrent.ArrayBlockingQueue使用’while’循环而不是’if’来调用await()?
他们使用while
不是if
来防止生产者/消费者模型中的经典线程竞争条件,并防止更罕见的虚假唤醒情况。
当(例如)多个消费者正在等待特定条件(如队列为空)并且通知条件时,另一个线程可能首先锁定并“窃取”添加到队列中的项目。 while
循环是必需的,以便线程在尝试对其进行排队之前确保队列中有一个项目。
示例代码
我写了一些示例代码和更多文档来演示竞争条件。
比赛条件的描述
查看您的具体代码,比赛如下:
- 消费者的线程#1位于while循环中的
await()
中,等待队列中的项目 - 生产者线程#2锁定队列
- 线程#3,一个消费者,完成消耗最后一项,调用
get()
,锁定队列,并且必须等待#2解锁(它不等待hasItems
但它正在等待获取lock
) - 线程#2,将一个项添加到队列中并调用
hasItems.signal()
来通知某人那里有一个项目 - 线程#1被唤醒并锁定队列, 必须等待 #2解锁
- 线程#2解锁
- 线程#3 在线程#1之前等待锁定,因此它首先锁定队列,进入while循环并将#1通知的项目出列,然后解锁
- 线程#1现在能够锁定。 如果它只是一个
if
语句,它将继续并尝试从一个空列表中出列,这将抛出ArrayIndexOutOfBoundsException
或其他东西。
while
语句必要的原因是处理这些竞争条件。 在上面的步骤8中,有一段while
,线程#1将循环回到测试以查看队列中是否有项目,发现没有项目,然后返回等待。
这是一个经典的问题,惹恼了许多重新编程的程序员。 例如,O’Reilly pthreads圣经的初始版本有没有 while循环的示例代码,必须重新发布。
对于某些线程系统,系统更容易唤醒所有条件而不是已经发出信号的特定条件,因此可能发生“虚假唤醒” 。 while
循环也可以防止这种情况发生。
您实施的另一个问题是可能丢失的“信号”。 如果仔细观察处理InterruptedException的jdk impls,它们中的一些会在返回之前发出信号。 这是因为可以选择一个线程用信号通知然后最终被中断,在这种情况下该信号将丢失。 因此,当被中断时,jdk会在拯救之前重新发出信号。 由于该线程实际上可能已经或可能没有接收到信号,这也可能导致“虚假唤醒”(如前所述)。
也许错过了你的观点,但原始代码使用了一段时间而不是因为可能有多个线程正在侦听/消耗队列…
这样做的主要原因是,当一个线程被通知时,它不能立即采取行动,它必须首先获得锁定。 并且无法保证它将成为获得锁定的下一个线程。
当线程在等待时收到通知时,它没有锁。 (它在开始等待时释放了锁。)在它离开wait方法之前,它必须重新获取锁。 它没有优先于任何其他争用锁的线程,它完全可能是其他一些线程可能首先到达那里。
无论那些干预线程做什么都可以改变被访问对象的状态,可能使通知无关紧要。 因此,一旦通知的线程成功重新获取锁,它就需要再次检查条件,以便validation它被通知的条件仍然是真的。 while循环在那里,以便线程可以在锁定后再次检查条件。
除此之外还有其他原因使用while循环检查您正在等待的条件是否存在:
-
从你的线程推断停止等待它必然已经得到通知是无效的; 这是“虚假的唤醒”。
-
此外,如果通知可能涉及多个事项,那么您需要再次检查条件,以确保您感兴趣的事件是您被唤醒的内容。