【问题标题】:What is the latency of a BlockingQueue's take() method?BlockingQueue 的 take() 方法的延迟是多少?
【发布时间】:2013-04-22 18:58:12
【问题描述】:

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

【问题讨论】:

    标签: java multithreading concurrency queue blocking


    【解决方案1】:

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

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

    【讨论】:

      【解决方案2】:

      好吧,这里是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(),其中:

      使当前线程等待,直到它发出信号或 打断了。

      与此条件关联的锁被原子释放,并且 当前线程出于线程调度目的而被禁用,并且 处于休眠状态,直到发生以下四种情况之一:

      1. 其他一些线程为此条件调用信号方法,并且 当前线程恰好被选为要被唤醒的线程;或
      2. 其他一些线程为此条件调用 signalAll 方法;或
      3. 其他一些线程中断了当前线程,并且中断了 支持线程挂起;或
      4. 发生“虚假唤醒”。

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

      【讨论】:

      • 非常感谢。您知道 Zim-Zam 在另一个答案中提到的公平属性与您提供的解释有何关系?
      【解决方案3】:

      您可以假设 take() 将被通知它可以在您的操作系统可以在线程之间传递这样的信号时立即唤醒。注意:您的操作系统将涉及最坏的情况。通常这是 1 到 10 微秒,在极少数情况下为 100 甚至 1000 微秒。注意:Thread.sleep 将等待至少 1000 微秒,而 25 毫秒是 25,000 微秒,所以我希望您能清楚地看到差异。

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

      【讨论】:

      • 谢谢彼得,像往常一样+1...但是我的例子当然不是具体的 25 毫秒:它更多的是了解两者是如何工作的。例如,如果 take() 需要 10 微秒,如果我可以使用 10 微秒的假设值调用 Thread.sleep,那么这两种技术会有什么不同? :)
      • Thread.sleep 会在你指定的 take 之后唤醒,但是 take() 在你触发它唤醒之后 10 微秒才会唤醒。不幸的是,没有办法让睡眠时间少于 1 毫秒(尽管有方法似乎可以做到这一点)
      【解决方案4】:

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

      【讨论】:

        【解决方案5】:

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

        【讨论】:

          猜你喜欢
          • 2022-01-25
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2011-06-02
          • 1970-01-01
          • 1970-01-01
          • 2014-06-16
          • 1970-01-01
          相关资源
          最近更新 更多