1. 概述
✅ LinkedBlockingQueue
和 ConcurrentLinkedQueue
是 Java 并发编程中最常用的两种线程安全队列。虽然它们都能用于多线程环境下的数据传递,但内部实现机制和适用场景差异显著。
本文将深入剖析两者的设计特点、行为差异及性能权衡,帮助你在实际开发中做出更合理的选择,避免踩坑。
2. LinkedBlockingQueue:可选有界的阻塞队列
LinkedBlockingQueue
是一个可选有界的阻塞队列(blocking queue),意味着你可以指定其最大容量,也可以让它无界。
创建方式
// 有界队列,最多容纳 100 个元素
BlockingQueue<Integer> boundedQueue = new LinkedBlockingQueue<>(100);
// 无界队列,容量默认为 Integer.MAX_VALUE
BlockingQueue<Integer> unboundedQueue = new LinkedBlockingQueue<>();
// 从已有集合初始化
Collection<Integer> listOfNumbers = Arrays.asList(1, 2, 3, 4, 5);
BlockingQueue<Integer> queue = new LinkedBlockingQueue<>(listOfNumbers);
⚠️ 虽然无界队列看似“无限”,但一旦内存耗尽,就会抛出 OutOfMemoryError
—— 这在高吞吐场景下是个隐藏炸弹。
阻塞特性
它实现了 BlockingQueue
接口,具备阻塞能力:
- 当队列满时,
put()
操作会阻塞生产者线程,直到有空间; - 当队列空时,
take()
操作会阻塞消费者线程,直到有数据。
ExecutorService executorService = Executors.newFixedThreadPool(1);
LinkedBlockingQueue<Integer> queue = new LinkedBlockingQueue<>();
executorService.submit(() -> {
try {
// 空队列下调用 take,线程会被挂起
queue.take();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
✅ 这种“阻塞等待”非常适合典型的生产者-消费者模型,比如任务调度、消息缓冲等。
性能代价
但阻塞的背后是锁竞争。LinkedBlockingQueue
使用 两把锁(two-lock queue 算法):
putLock
:保护入队操作takeLock
:保护出队操作
这使得生产者和消费者可以并行操作,减少锁争用。但在高并发下,频繁的 put/take
仍可能成为瓶颈。
3. ConcurrentLinkedQueue:无锁的高性能非阻塞队列
ConcurrentLinkedQueue
是一个无界、线程安全、非阻塞的队列,适用于极高并发场景。
创建方式
// 创建空队列
ConcurrentLinkedQueue<Integer> queue = new ConcurrentLinkedQueue<>();
// 从集合初始化
Collection<Integer> listOfNumbers = Arrays.asList(1, 2, 3, 4, 5);
ConcurrentLinkedQueue<Integer> queue2 = new ConcurrentLinkedQueue<>(listOfNumbers);
非阻塞设计
❌ 它不实现 BlockingQueue
接口,因此没有 put()
或 take()
方法。
取而代之的是:
offer(E e)
:插入元素,永不阻塞,失败返回false
poll()
:取出元素,队列为空时返回null
,不会阻塞
int element = 1;
ExecutorService executorService = Executors.newFixedThreadPool(2);
ConcurrentLinkedQueue<Integer> queue = new ConcurrentLinkedQueue<>();
Runnable offerTask = () -> queue.offer(element);
Callable<Integer> pollTask = () -> {
// 必须先 peek 判断是否存在元素,避免空轮询
while (queue.peek() != null) {
return queue.poll();
}
return null;
};
executorService.submit(offerTask);
Future<Integer> result = executorService.submit(pollTask);
assertThat(result.get(), is(equalTo(element)));
⚠️ 注意:由于是非阻塞队列,消费者需要自行处理 null
值,不能依赖“阻塞等待”。
实现原理
- ✅ 基于 CAS(Compare-And-Swap) 实现
- ✅ 使用 Michael & Scott 算法,保证无锁(lock-free)
- 生产者之间会有 CAS 冲突,但生产者与消费者基本不争抢资源
这种设计在高并发下性能远超基于锁的队列,但代价是代码复杂度更高。
4. 共同点
尽管机制不同,两者仍有以下共性:
- ✅ 都实现了
Queue
接口 - ✅ 内部使用链表节点(linked nodes)存储元素
- ✅ 天然支持多线程并发访问,无需额外同步
这些特性使它们成为并发场景下的首选队列实现。
5. 核心差异对比
特性 | LinkedBlockingQueue | ConcurrentLinkedQueue |
---|---|---|
阻塞性 | ✅ 阻塞队列,实现 BlockingQueue |
❌ 非阻塞,不实现 BlockingQueue |
队列大小 | 可选有界(可指定容量) | ❌ 仅无界,无法限制大小 |
锁机制 | 基于锁(lock-based) | ✅ 无锁(lock-free),依赖 CAS |
核心算法 | 两锁队列(two-lock queue) | Michael & Scott 算法 |
内部实现 | putLock 和 takeLock 分离 |
所有操作通过原子 CAS 完成 |
空队列行为 | take() 阻塞线程 |
poll() 返回 null ,不阻塞 |
关键理解点
LinkedBlockingQueue
的poll()
和offer()
在队列未满/非空时不会阻塞,只有take()
/put()
才会阻塞。ConcurrentLinkedQueue
的所有操作都完全无锁,性能更高,但需自行处理空值逻辑。- 如果你需要“背压”(backpressure)控制或限流,
LinkedBlockingQueue
更合适; - 如果你追求极致吞吐且能接受无界风险,
ConcurrentLinkedQueue
是更好的选择。
6. 总结
选择哪个队列,本质上是在 “阻塞 vs 非阻塞”、“有界 vs 无界”、“锁 vs CAS” 之间做权衡。
场景 | 推荐队列 |
---|---|
生产者-消费者模型,需阻塞等待 | LinkedBlockingQueue |
高并发日志收集、事件广播 | ConcurrentLinkedQueue |
需要控制内存使用、防 OOM | LinkedBlockingQueue (有界) |
极致性能,允许无界增长 | ConcurrentLinkedQueue |
📌 最后提醒:不要因为 ConcurrentLinkedQueue
性能高就盲目替换。合适的才是最好的。理解底层机制,才能写出真正高效的并发代码。
示例代码已托管至 GitHub:https://github.com/example/java-concurrent-queues