1

我想了解它是如何take()工作的,以及它是否是一种合适的方法来消耗被推送到队列中的“快速”元素。

请注意,为了理解它的工作原理,我在这里没有考虑观察者模式:我知道我可以使用该模式对事件“快速做出反应”,但这不是我的问题所在。

例如,如果我有一个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()如何工作有关的大问题,我认为回答我的各种问题(至少是一个有意义的问题)将帮助我更好地理解这一切。

4

5 回答 5

1

您可以假设只要您的操作系统可以在线程之间传递这样的信号,就会通知 take() 它可以唤醒。注意:您的操作系统将涉及最坏的情况。通常这是 1 - 10 微秒,在极少数情况下为 100 或什至 1000 微秒。注意: Thread.sleep 将等待至少 1000 微秒,而 25 毫秒是 25,000 微秒,所以我希望差异对您来说是显而易见的。

避免罕见但长时间上下文切换的唯一真正方法是忙于等待亲和锁 CPU。(这会为您的线程分配一个 CPU)如果您的应用程序对延迟敏感,更简单的解决方案是根本不在线程之间传递工作。;)

于 2013-04-22T20:05:34.803 回答
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 关联的锁被自动释放,并且当前线程出于线程调度目的而被禁用并处于休眠状态,直到发生以下四种情况之一:

  1. 其他某个线程为此Condition调用了signal方法,而当前线程恰好被选为要被唤醒的线程;或者
  2. 其他一些线程为此 Condition 调用 signalAll 方法;或者
  3. 其他一些线程中断当前线程,支持中断线程挂起;或者
  4. 发生“虚假唤醒”。

当另一个线程将某些东西放入队列时,它会调用signal,这会唤醒等待消费该队列中的项目的线程之一。这应该比你的peek/sleep循环更快。

于 2013-04-22T19:31:24.157 回答
1

在内部,take等待在方法notEmpty中发出的条件insert;换句话说,等待线程进入睡眠状态,并在insert. 这应该很快醒来。

一些阻塞队列,例如ArrayBlockingQueueand SynchronousQueue,有一个接受队列公平属性的构造函数;传入true应该防止线程卡住,take,否则这是可能的。(该参数指定底层ReentrantLock是否公平。)

于 2013-04-22T19:31:00.797 回答
0

由于涉及两个线程,具有假设的微peek/sleep纳米睡眠实现的 / 不会有太大差异,take()因为它们都涉及通过主内存将信息从一个线程传递到下一个线程(使用volatile写入/读取和健康数量的 CAS) ,除非 JVM 找到其他方法来进行线程间同步。您可以尝试使用两个 s 和两个线程来实现基准测试BlockingQueue,每个线程充当一个队列的生产者和另一个队列的消费者,并来回移动令牌,将其从一个队列中取出并offering 到下一个队列。然后你可以看到他们生产/消费的速度有多快,并将其与peek/sleep. 我猜性能很大程度上取决于在每个令牌上花费的工作量(在这种情况下为零,所以我们测量纯开销)和 CPU 到内存的距离。以我的经验,单 CPU 比多插槽机器领先。

于 2013-04-22T21:06:24.157 回答
0

不同之处在于第一个线程的睡眠时间最长为 25 毫秒,而第二个线程根本不会浪费任何时间。

于 2013-04-22T23:15:59.603 回答