我想了解take()
是如何工作的,以及它是否是一种合适的方法来消耗推送到队列中的“快速”元素。
请注意,为了理解它是如何工作的,我不会在这里考虑观察者模式:我知道我可以使用该模式对事件“快速反应”,但这不是我的问题所在。 / p>
例如,如果我有一个BlockingQueue
(大多数是空的)并且线程“卡住”等待一个元素被推送到该队列以便它可以被消耗,那么什么是最好的方法来最小化在元素被推送到队列的那一刻和它被消耗的那一刻之间花费的时间(减少延迟)?
例如,执行此操作的线程之间的区别是什么:
while( true ) {
elem = queue.peek();
if ( elem == null ) {
Thread.sleep( 25 ); // prevents busy-looping
} else {
... // do something here
}
}
另一个人这样做:
while ( true ) {
elem = queue.take();
... // do something with elem here
}
(我认为这是为了简化我们可以忽略的讨论异常的事情!?)
当您呼叫take()
并且队列为空时,幕后发生了什么? JVM不知何故必须“隐藏”引擎盖下的线程,因为它不能忙于循环,不断检查队列中是否有东西?是 take()使用引擎盖下的一些CAS操作吗?如果是这样,是什么决定了 take()多久调用一次CAS操作?
什么东西突然出现在队列中?那个帖子在take()
上以某种方式阻止“通知”它应该立即采取行动的方式如何?
最后,在应用程序的生命周期内,在BlockingQueue上将一个线程“卡在” take()上是“常见的”吗?
关于阻止 take()如何工作的所有一个大问题,我认为回答我的各种问题(至少是有意义的问题)会帮助我更好地理解这一切。
答案 0 :(得分:1)
在内部,take
等待notEmpty
条件,该条件在insert
方法中发出信号;换句话说,等待线程进入休眠状态,并在insert.
上醒来。这应该很快。
一些阻止队列,例如ArrayBlockingQueue
和SynchronousQueue
有一个接受队列公平属性的构造函数;传入true
应该可以防止线程卡在take,
上,否则这是可能的。 (此参数指定基础ReentrantLock是否公平。)
答案 1 :(得分:1)
嗯,这是LinkedBlockingQueue<E>.take()
的实现:
public E take() throws InterruptedException { E x; int c = -1; final AtomicInteger count = this.count; final ReentrantLock takeLock = this.takeLock; takeLock.lockInterruptibly(); try { while (count.get() == 0) { notEmpty.await(); } x = dequeue(); c = count.getAndDecrement(); if (c > 1) notEmpty.signal(); } finally { takeLock.unlock(); } if (c == capacity) signalNotFull(); return x; }
当队列为空时,调用notEmpty.await()
,其中:
使当前线程等待,直到发出信号或 中断。
与此条件关联的锁是原子释放的 当前线程因线程调度而被禁用 在四件事之一发生之前,它处于休眠状态:
- 其他一些线程调用此条件的信号方法和 恰好选择当前线程作为要唤醒的线程;或
- 其他一些线程为此Condition调用signalAll方法;或
- 其他一些线程会中断当前线程,并中断 支持线程悬挂;或
- 发生“虚假唤醒”。
醇>
当另一个线程在队列中放入某些东西时,它会调用signal
,它会唤醒其中一个等待从此队列中使用项目的线程。这应该比peek
/ sleep
循环更快。
答案 2 :(得分:1)
你可以假设,当你的操作系统可以在线程之间传递这样的信号时,会通知take()它可以唤醒它。注意:您的操作系统将涉及最坏的情况。通常这是1-10微秒,在极少数情况下,在极少数情况下为100微秒甚至1000微秒。注意:Thread.sleep将等待至少1000微秒,25毫秒为25,000微秒,所以我希望你的区别是显而易见的。
避免罕见但长时间上下文切换的唯一方法是忙于等待亲和力锁CPU。 (这会为您的线程分配CPU)如果您的应用程序是延迟合理的,那么更简单的解决方案就是不要在线程之间传递工作。 ;)
答案 3 :(得分:0)
由于涉及两个线程,具有假设微/纳睡眠实现的peek
/ sleep
与take()
的差异不会太大,因为它们都涉及从一个线程传递信息除非JVM找到其他方法进行线程间同步,否则通过主内存(使用volatile
写入/读取和健康数量的CAS)到下一个。您可以尝试使用两个BlockingQueue
和两个线程来实现基准测试,每个线程作为一个队列的生产者和另一个队列的消费者,并来回移动令牌,从一个队列中取出offer
到下一个。然后,您可以看到它们产生/消费的速度有多快,并将其与peek
/ sleep
进行比较。我想性能很大程度上取决于每个令牌花费的工作量(在这种情况下为零,因此我们测量纯粹的开销)以及CPU与内存的距离。根据我的经验,单CPU在多插槽机器之前就已经出现了。
答案 4 :(得分:0)
区别在于第一个线程的睡眠时间长达25ms,而第二个线程根本不会浪费任何时间。