1. 概述
LinkedBlockingQueue和ConcurrentLinkedQueue是Java中最常用的两个并发队列。尽管这两个队列通常被用作并发数据结构,但它们之间存在细微的特征和行为差异。
在这个简短的教程中,我们将讨论这两个队列并解释它们的异同。
2. LinkedBlockingQueue
LinkedBlockingQueue是一个可选的有界阻塞队列实现,这意味着可以根据需要指定队列大小。
让我们创建一个最多可包含100个元素的LinkedBlockingQueue:
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类实现了BlockingQueue接口,该接口为它提供了阻塞特性。
阻塞队列表示如果队列已满(当队列有界时)或变为空,则该队列将阻塞访问线程。如果队列已满,则添加新元素将阻塞访问线程,除非有空间可用于新元素。同样,如果队列为空,则访问元素会阻塞调用线程:
ExecutorService executorService = Executors.newFixedThreadPool(1);
LinkedBlockingQueue<Integer> queue = new LinkedBlockingQueue<>();
executorService.submit(() -> {
try {
queue.take();
}
catch (InterruptedException e) {
// exception handling
}
});
在上面的代码片段中,我们正在访问一个空队列。因此,take方法会阻塞调用线程。
LinkedBlockingQueue的阻塞特性与一些成本相关联,这种成本是因为每个put或take操作都在生产者或消费者线程之间进行锁竞争。因此,在具有许多生产者和消费者的场景中,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从队列中检索一个元素。pollTask还会首先检查队列中的元素,因为ConcurrentLinkedQueue是非阻塞的并且可能返回空值。
4. 相似之处
LinkedBlockingQueue和ConcurrentLinkedQueue都是队列实现,并且具有一些共同特征。让我们讨论一下这两个队列的相似之处:
- 两者都实现了Queue接口
- 他们都使用链表节点来存储其元素
- 两者都适用于并发访问场景
5. 差异
尽管这两个队列有一定的相似之处,但也存在实质性的特征差异:
功能 | LinkedBlockingQueue | ConcurrentLinkedQueue |
---|---|---|
阻塞 | 它是一个阻塞队列,实现了BlockingQueue接口 | 它是一个非阻塞队列,没有实现BlockingQueue接口 |
队列大小 | 它是一个可选的有界队列,这意味着在创建过程中可以定义队列大小 | 它是一个无界队列,在创建过程中没有指定队列大小的规定 |
锁定性质 | 它是一个基于锁的队列 | 它是一个无锁队列 |
算法 | 它基于双锁队列算法实现其锁定 | 它依赖于Michael&Scott算法实现无阻塞、无锁队列 |
实现 | 在双锁队列算法机制中,LinkedBlockingQueue使用了两种不同的锁-putLock和takeLock。put/take操作使用第一种锁类型,而take/poll操作使用另一种锁类型 | 它使用CAS(比较和交换)进行操作 |
阻塞行为 | 它是一个阻塞队列。因此,当队列为空时,它会阻塞访问线程 | 当队列为空时返回null,不会阻塞访问线程 |
6. 总结
在本文中,我们了解了LinkedBlockingQueue和ConcurrentLinkedQueue。
首先,我们分别讨论了这两个队列的实现以及它们的一些特性。然后,我们看到了这两个队列实现之间的相似之处。最后,我们探讨了这两种队列实现之间的差异。
与往常一样,本教程的完整源代码可在GitHub上获得。