1. 概述

LinkedBlockingQueueConcurrentLinkedQueue 是 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 算法
内部实现 putLocktakeLock 分离 所有操作通过原子 CAS 完成
空队列行为 take() 阻塞线程 poll() 返回 null,不阻塞

关键理解点

  • LinkedBlockingQueuepoll()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


原始标题:LinkedBlockingQueue vs ConcurrentLinkedQueue