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();
}
为什么会有一个循环?
putLock将所有put线程都关闭。
当等待线程持有putLocker时,没有线程可以增加'count'。
await
有一个基本属性(适用于通过synchronized
内部锁定并使用Object.wait
),你必须明白:
当你调用await
时,你释放锁定这个Condition
与¹相关联。没有办法绕过它,否则,没有人可以获得锁定,使条件得到满足,并在其上调用signal
。
当您的等待线程发出信号时,它不会立即获得锁定。这是不可能的,因为调用signal
的线程仍然拥有它。相反,接收器将尝试重新获取锁,与调用lockInterruptibly()
没有太大区别。
但是这个线程不一定是尝试获取锁的唯一线程。它甚至不必是第一个。另一个线程可能在发出信号之前到达put
并等待lockInterruptibly()
的锁定。因此,即使锁是公平的(通常锁不通),信号线也没有优先权。即使您给出了信号通知的线程优先级,也可能出于不同的原因发出多个线程的信号。
因此,到达put
的另一个线程可以在发出信号的线程之前获得锁定,发现存在空间,并且存储该元素而不必费心地处理信号。然后,在发信号通知线程获得锁定时,不再满足条件。因此,信号通知的线程永远不能仅仅因为它接收到信号而依赖于条件的有效性,因此必须重新检查条件并且如果未满足则再次调用await
。
这使得在循环中检查条件是使用await
的标准习惯用法,如the Condition
interface中所记录的,以及Object.wait
用于使用内在监视器的情况,仅用于完整性。换句话说,这甚至不是特定于API的特定。
由于必须在循环中预先检查和重新检查条件,因此规范甚至允许虚假唤醒,即从等待操作返回而没有实际接收信号的线程的事件。这可以简化某些平台的锁实现,同时不改变必须使用锁的方式。
¹重要的是要强调,当持有多个锁时,只释放与条件相关的锁。
当LinkedBlockingQueue的容量已满时,循环函数(*)阻塞称为put方法的线程。当另一个线程调用take(或poll)方法时,队列中将有新元素的空间,take方法将发出notFull
条件的信号,等待的线程将被唤醒,并可以将项目放入队列。
(*)循环的条件是保证没有发生虚假唤醒。
@Holder的答案是正确的,但我想添加有关以下代码和问题的更多详细信息。
putLock.lockInterruptibly();
try {
while (count.get() == capacity) {
notFull.await();
}
...
为什么会有一个循环? putLock将所有put线程都关闭。
while循环是这个代码模式的关键部分,它确保当线程从notFull
上的信号中唤醒时,它确保另一个线程没有首先到达那里并重新填充缓冲区。
有一点重要的是要认识到notFull
被定义为putLock
的一个条件:
private final Condition notFull = putLock.newCondition();
当线程调用notFull.await()
时,它将解锁putLock
,这意味着多个线程可以同时运行notFull.await()
。一个线程只会在调用notFull.signal()
(或signalAll()
)后尝试重新获取锁。
如果线程A是BLOCKED
试图获得putLock
而线程B是WAITING
上的notFull
,则会出现竞争条件。如果一个线程C从队列中删除了某些东西并发出notFull
信号,那么线程B将被从等待队列中取出并放入putLock
上的阻塞队列中,但不幸的是,它将在已经被阻塞的线程A后面。所以一旦putLock
解锁,线程A将获得putLock
并将一些东西放入队列,再次填充它。当线程B最终获取putLock
时,它需要再次测试以查看在放入(和溢出)队列之前是否仍有可用空间。这就是为什么while
是必要的。
正如@Holder所提到的,while循环的第二个原因是防止虚假唤醒,当某个条件被人工发出信号时,这种唤醒会在某些线程架构下发生。例如,在某些架构下,由于操作系统的限制,任何条件下的信号都会发出所有条件信号。