LinkedBlockingQueue与ConcurrentLinkedQueue的比较

评论 0 浏览 0 2020-05-20

1.绪论

LinkedBlockingQueueConcurrentLinkedQueue是Java中最常使用的两个并发队列。尽管这两种队列都经常被用作并发数据结构,但它们之间存在着微妙的特征和行为上的差异。

在这个简短的教程中,我们将讨论这两个队列,并解释它们的相似之处和不同之处。

2. LinkedBlockingQueue

LinkedBlockingQueue 是一个可选的有边界的阻塞队列实现,意味着如果需要的话,可以指定队列的大小。

让我们创建一个LinkedBlockingQueue,它最多可以包含100个元素。

BlockingQueue<Integer> boundedQueue = new LinkedBlockingQueue<>(100);

我们也可以创建一个无界的LinkedBlockingQueue,只需不指定大小即可。

BlockingQueue<Integer> unboundedQueue = new LinkedBlockingQueue<>();

无界队列意味着队列的大小在创建时没有被指定。因此,队列可以随着元素的加入而动态增长。然而,如果没有剩余的内存,那么队列就会抛出一个java.lang.OutOfMemoryError

我们也可以从一个现有的集合中创建一个LinkedBlockingQueue

Collection<Integer> listOfNumbers = Arrays.asList(1,2,3,4,5);
BlockingQueue<Integer> queue = new LinkedBlockingQueue<>(listOfNumbers);

LinkedBlockingQueue class 实现了BlockingQueue接口,这为它提供了阻塞的性质

阻塞队列表明,如果队列满了(当队列有界时)或变成空了,队列会阻塞访问线程。如果队列是满的,那么添加一个新的元素将阻塞访问线程,除非有可用的空间给新元素。同样地,如果队列是空的,那么访问一个元素就会阻塞调用线程。

ExecutorService executorService = Executors.newFixedThreadPool(1);
LinkedBlockingQueue<Integer> queue = new LinkedBlockingQueue<>();
executorService.submit(() -> {
  try {
    queue.take();
  } 
  catch (InterruptedException e) {
    // exception handling
  }
});

在上面的代码片段中,我们正在访问一个空队列。因此,take 方法会阻塞调用线程。

LinkedBlockingQueen 的阻塞功能与一些成本有关。这个代价是因为每一个puttake的操作都是生产者或消费者线程之间的锁争夺。因此,在有许多生产者和消费者的情况下,put和take操作可能会比较慢。

3. ConcurrentLinkedQueue

ConcurrentLinkedQueue 是一个无界的、线程安全的、非阻塞的队列。

让我们来创建一个空的ConcurrentLinkedQueue

ConcurrentLinkedQueue queue = new ConcurrentLinkedQueue();

我们也可以从一个现有的集合中创建一个ConcurrentLinkedQueue

Collection<Integer> listOfNumbers = Arrays.asList(1,2,3,4,5);
ConcurrentLinkedQueue<Integer> queue = new ConcurrentLinkedQueue<>(listOfNumbers);

LinkedBlockingQueue不同,ConcurrentLinkedQueue是一个非阻塞队列。因此,一旦队列为空,它就不会阻塞线程。相反,它返回null。因为它是无界的,如果没有多余的内存来添加新的元素,它会抛出一个java.lang.OutOfMemoryError

除了非阻塞外,ConcurrentLinkedQueue 还具有额外的功能。

在任何生产者-消费者场景中,消费者都不会与生产者发生竞争;然而,多个生产者将相互竞争:

int element = 1;
ExecutorService executorService = Executors.newFixedThreadPool(2);
ConcurrentLinkedQueue<Integer> queue = new ConcurrentLinkedQueue<>();

Runnable offerTask = () -> queue.offer(element);

Callable<Integer> pollTask = () -> {
  while (queue.peek() != null) {
    return queue.poll().intValue();
  }
  return null;
};

executorService.submit(offerTask);
Future<Integer> returnedElement = executorService.submit(pollTask);
assertThat(returnedElement.get().intValue(), is(equalTo(element)));

第一个任务,offerTask,向队列添加一个元素,第二个任务,pollTask,从队列中检索一个元素。轮询任务另外先检查队列中的元素,因为ConcurrentLinkedQueue 是非阻塞的,可以返回null

4.相似性

LinkedBlockingQueue ConcurrentLinkedQueue 都是队列的实现,并且有一些共同的特征。让我们来讨论一下这两个队列的相似之处。

  1. 两者都实现了Queue接口
  2. 它们都使用链接节点来存储它们的元素
  3. 两者都适用于并发访问的情况

5.差异

虽然这两个队列有某些相似之处,但也有实质性的特征差异。

特征 LinkedBlockingQueue ConcurrentLinkedQueue
阻塞性质 它是一个阻塞队列,并实现了BlockingQueue接口。 它是一个非阻塞队列,没有实现BlockingQueue接口
队列大小 它是一个可选择的有界队列,这意味着在创建过程中,可以定义队列大小 它是一个无界的队列,在创建过程中没有规定指定队列的大小。
锁性质 它是一个基于锁的队列 它是一个无锁的队列
算法 它基于双锁队列算法实现了它的锁定 它依赖于Michael & Scott算法,用于非阻塞、无锁的队列
实现 双锁队列算法机制中,LinkedBlockingQueue 使用两种不同的锁—— putLocktakeLockput/take操作使用第一种锁类型,take/poll 操作使用其他锁类型 它使用CAS(Compare-And-Swap)进行操作
阻塞行为 它是一个阻塞队列。因此,当队列为空时,它会阻止访问线程。 当队列为空时,它不会阻塞访问线程,并返回null

6.结论

在这篇文章中,我们了解了LinkedBlockingQueue ConcurrentLinkedQueue.

首先,我们单独讨论了这两个队列的实现和它们的一些特点。然后,我们看到了这两个队列实现之间的相似之处。最后,我们探讨了这两个队列实现之间的差异。

像往常一样,这些例子的源代码可以在GitHub上找到。

最后更新2023-03-15
0 个评论
标签