Java中几个常用并发队列比较 | Baeldung


在多线程应用程序中,队列需要处理多个并发的生产者-消费者方案。正确选择并发队列对于在我们的算法中实现良好性能至关重要。 
首先,我们将看到阻塞队列和非阻塞队列之间的一些重要区别。然后,我们将看一些实现和最佳实践。
 
BlockingQueue提供了一种简单的线程安全机制。在此队列中,线程需要等待队列的可用性。生产者将在添加元素之前等待可用容量,而消费者将等待直到队列为空。为了实现这种阻塞机制,BlockingQueue接口在常规Queue函数的基础上提供了两个函数:put和take。这些功能等效于标准Queue中的add和remove。
 
ArrayBlockingQueue
此队列在内部使用数组。因此,它是一个有界队列,这意味着它具有固定的大小。适合生产者/消费者比率通常很低情况,我们将耗时的任务分配给多个worker。由于此队列不能无限增长,因此如果出现内存问题,需要将大小限制将作为安全阈值。
ArrayBlockingQueue对put和take操作都使用一个锁。这样可以确保不覆盖条目,但会降低性能。
 
LinkedBlockingQueue
LinkedBlockingQueue使用链表变体,其中每个队列项目是一个新的节点。虽然这使队列在原则上不受限制,但仍然具有Integer.MAX_VALUE的硬限制。我们可以使用构造函数LinkedBlockingQueue(int capacity)设置队列大小。
队列使用不同的锁进行put和take操作。因此两种操作可以并行完成并提高了吞吐量。
由于LinkedBlockingQueue可以是有界的或无界的,为什么我们还要使用ArrayBlockingQueue?每次在队列中添加或删除项目时,LinkedBlockingQueue都需要分配和取消分配节点。因此,如果队列快速增长和快速收缩,则  ArrayBlockingQueue可能是更好的选择。
据说LinkedBlockingQueue的性能是不可预测的。换句话说,我们始终需要剖析我们的方案以确保我们使用正确的数据结构。
 
PriorityBlockingQueue
当我们需要按特定顺序消费数据时,PriorityBlockingQueue是我们的首选解决方案。为此,PriorityBlockingQueue使用基于数组的二进制堆。
尽管在内部使用单个锁定机制,但是take操作可以与put操作同时进行。使用简单的自旋锁可以实现这一点。
一个典型的用例是使用具有不同优先级的任务。我们不希望低优先级的任务代替高优先级的任务。
 
DelayQueue
当使用者只能take过期的数据项目时,我们使用DelayQueue 。有趣的是,它在内部使用PriorityQueue来按数据项目的到期时间对其进行排序。
由于这不是通用队列,因此它无法涵盖ArrayBlockingQueue或LinkedBlockingQueue那样多的场景。例如,我们可以使用此队列来实现一个简单的事件循环,类似于在NodeJS中找到的事件循环。我们将异步任务放在队列中,以便在它们到期时进行后续处理。
 
LinkedTransferQueue
LinkedTransferQueue引入一个transfer 方法。尽管其他队列通常在生产或消费数据项目时阻塞,但LinkedTransferQueue 允许生产者等待数据项目的消费。
当我们需要保证放入队列中的某个特定项目已被消费者take拿走时,可以使用LinkedTransferQueue。同样,我们可以使用此队列实现简单的反压算法。实际上,通过阻止生产者直到消费,消费者可以驱动所产生的消息流。
 
SynchronousQueue
普通队列通常包含许多数据项目,但SynchronousQueue最多始终只有一个项目。换句话说,我们需要将SynchronousQueue视为在两个线程之间交换某些数据的简单方法。
当我们有两个需要访问共享状态的线程时,我们通常将它们与CountDownLatch或其他同步机制同步。通过使用SynchronousQueue,我们可以避免线程的这种手动同步。
 
ConcurrentLinkedQueue
ConcurrentLinkedQueue是本文唯一的非阻塞队列,因此,它提供了一种“免等待”算法,其中add和poll保证是线程安全的,并立即返回。该队列使用CAS(Compare-And-Swap)代替锁。
在内部,它基于Maged M. Michael和Michael L. Scott的简单,快速和实用的非阻塞和阻塞并发队列算法
对于经常禁止使用阻塞数据结构的现代反应系统,它是理想的选择。
另一方面,如果我们的消费者最终陷入循环等待,我们可能应该选择阻塞队列作为更好的选择。