为什么 LinkedBlockingQueue 的 put() 中有一个 while 循环

why is there a while loop in put() of LinkedBlockingQueue

public void put(E e) throws InterruptedException {
    if (e == null) throw new NullPointerException();
    int c = -1;
    Node<E> node = new Node<E>(e);
    final ReentrantLock putLock = this.putLock;
    final AtomicInteger count = this.count;
    putLock.lockInterruptibly();
    try {
        while (count.get() == capacity) {
            notFull.await();
        }
        enqueue(node);
        c = count.getAndIncrement();
        if (c + 1 < capacity)
            notFull.signal();
    } finally {
        putLock.unlock();
    }
    if (c == 0)
        signalNotEmpty();
}

为什么会有 while 循环?

所有put线程都被putLock关闭了。

当等待线程持有 putLock 时,没有线程可以增加 'count'。

当LinkedBlockingQueue 容量满时,Loop 的函数(*) 正在阻塞调用put 方法的线程。当另一个线程调用 take(或 poll)方法时,队列中会有 space 新元素,take 方法将发出 notFull 条件,等待线程将被唤醒并可以放入项目到队列中。

(*) 循环条件是为了保证没有发生虚假唤醒。

https://en.wikipedia.org/wiki/Spurious_wakeup

await 有一个基本的 属性(它适用于通过 synchronized 和使用 Object.wait 的内在锁定),您必须了解:

当您调用 await 时,您正在 释放锁 Condition 关联至¹。没有办法绕过它,否则,没有人可以获取锁、满足条件并对其调用 signal

当您的等待线程收到信号时,它不会立即取回锁。这是不可能的,因为调用 signal 的线程仍然拥有它。相反,接收方将尝试重新获取锁,与调用 lockInterruptibly().

没有太大区别

但是这个线程不一定是唯一一个尝试获取锁的线程。它甚至不必是第一个。在 lockInterruptibly() 发出信号并等待锁定之前,另一个线程可能已经到达 put。因此,即使锁是公平的(通常锁不是),发出信号的线程也没有优先权。即使您给予信号线程优先权,也可能有多个线程因不同原因被信号通知。

所以到达 put 的另一个线程可以在收到信号的线程之前获得锁,发现有 space,然后存储元素,而不必担心信号。然后,当信号线程获得锁时,条件不再满足。因此,一个有信号的线程永远不能仅仅因为它收到一个信号就依赖条件的有效性,因此必须重新检查条件并在不满足时再次调用 await

这使得在循环中检查条件成为使用 await 的标准用法,如 the Condition interface, as well as Object.wait 中针对使用内部监视器的情况所记录的那样,只是为了完整性。换句话说,这甚至不特定于特定的 API.

由于无论如何都必须在循环中预先检查和重新检查条件,规范甚至允许 虚假唤醒,线程从等待返回的事件在没有实际接收到信号的情况下进行操作。这可能会简化某些平台的锁实现,同时不会改变必须使用锁的方式。

¹ 需要强调的是,持有多个锁时,释放与条件关联的锁。

@Holder 的回答是正确的,但我想添加有关以下代码和问题的更多详细信息。

putLock.lockInterruptibly();
try {
    while (count.get() == capacity) {
        notFull.await();
    }
    ...

why is there a while loop? All the putting thread is shut out by putLock.

while 循环是此代码模式的关键部分,可确保当线程被 notFull 上的信号唤醒时,它确保另一个线程不会先到达那里并重新填充缓冲区。

重要的是要认识到 notFull 被定义为 putLock 的条件:

private final Condition notFull = putLock.newCondition();

当线程调用notFull.await()时它会解锁putLock这意味着多个线程可以运行notFull.await()在同一时间。线程只会在调用 notFull.signal()(或 signalAll())后尝试重新获取锁。

如果线程 A BLOCKED 试图获取 putLock 并且线程 B 在 notFullWAITING,则会发生竞争条件。如果线程 C 从队列中移除某些东西并发出信号 notFull,线程 B 将从等待队列中取出并在 putLock 放入阻塞队列,但不幸的是,它将是 behind thread-A 已经被阻塞。所以一旦 putLock 被解锁,thread-A 将获取 putLock 并将一些东西放入队列中,再次填充它。当thread-B最终获取到putLock时,需要再次测试是否还有space可用,然后再放入(并溢出)队列。这就是为什么 while 是必要的。

while 循环的第二个原因,正如@Holder 也提到的,是为了防止在某些线程架构下当条件被人为发出信号时可能发生的虚假唤醒。例如,在某些架构下,由于 OS 限制,any 条件下的信号会发出 all 条件信号。