对比分析LinkedBlockingQueue和SynchronousQueue

缘起

最近在 review 同事代码时,看到其使用了org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor 来构建线程池,而没有使用 Java 类库,部分代码如下:

java 复制代码
@Bean
public ThreadPoolTaskExecutor queryToCExecutor() {
  ThreadPoolTaskExecutor poolTaskExecutor = new ThreadPoolTaskExecutor();
  //线程池维护线程的最少数量
  poolTaskExecutor.setCorePoolSize(5);

   //线程池维护线程的最大数量
  poolTaskExecutor.setMaxPoolSize(32);

  //允许的空闲时间,尽量复用,减少创建/销毁操作
  poolTaskExecutor.setKeepAliveSeconds(60);

  //缓存队列 0:不加入队列
  poolTaskExecutor.setQueueCapacity(0);
  poolTaskExecutor.setThreadGroupName("xxx");

  //阻塞加入队列
  poolTaskExecutor.setRejectedExecutionHandler(new QueryRejectedExecutionHandler());
  return poolTaskExecutor;
}

出于对注释中「缓存队列 0:不加入队列」的好奇,就看了下对应的源码(如下图),发现原来 Spring 会简单地根据容量值是否大于0而选择不同的Java阻塞队列作为其线程池的任务队列:队列容量大于0为 LinkedBlockingQueue,其他情况为 SynchronousQueue。看到这里,正好把我所了解到的有关这两个队列的内容梳理一下,是为温故而知新。


LinkedBlockingQueue面面观

  • 设计目的:为了消弭生产者和消费者之间的速度差异,提供一个安全的线程间缓存队列。

  • 实现机制

    • LinkedBlockingQueue是基于链表的 FIFO(First In First Out,即先进先出)阻塞队列,即其内部维护了一个单向链表,插入元素时在队列尾部追加节点,删除元素时在队列头部取出节点,以保证FIFO;

    • 对于生产/消费并发控制,内部定义了两个独立的锁:一把用于入队的 putLock,一把用于出队的 takeLock,这种锁分离机制,可以使生产者的入队操作和消费者的出队操作可以并行。

    • 同时,为了协调生产者/消费者,其还配备了对应的条件变量:在队列满时阻塞生产者的 notFullnotFull=putLock.newCondition()),以及在队列空时阻塞消费者的 notEmptynotEmpty=takeLock.newCondition())。当生产者插入元素使队列从空变为非空时,会 signal notEmpty 通知等待的消费者线程;类似地,当消费者移除元素使队列从满变为未满时,会 signal notFull 通知等待的生产者线程。

  • 容量特性

    • 默认为无界队列(容量为Integer.MAX_VALUE),生产者不会因为队列满了而阻塞,实际上仍然受内存限制

    • 有界模式,可指定容量,如 new LinkedBlockingQueue(100);

  • 操作特性

    • 支持异步操作,生产者可以独立插入元素(如果队列未满),消费者可以独立取出元素(如果队列非空);

    • 插入/删除时间复杂度为 O(1),但遍历操作(如 contains())时间复杂度为 O(n)

  • 适用场景

    • 固定大小线程池(如Executors.newFixedThreadPool())使用无界的 LinkedBlockingQueue 存放多余任务;

    • 通用生产-消费者模型需要缓冲时;

    • 适合生产消费速率不一致、有突发流量需要缓冲的场景。

其在 JDK 实现的类UML 如下图:


SynchronousQueue面面观

  • 设计目的:提供线程间同步交换数据的机制。

  • 实现机制:

    • SynchronousQueue底层没有使用传统的数据结构,内部可理解为维护了两个队列/栈结构:一个等待中的生产者线程集合和一个等待中的消费者线程集合。

    • 当有生产者线程执行 put 时,如果此时有消费者线程在等待获取元素,双方直接配对完成元素交接;如果没有消费者等待,那么生产者线程就会自己阻塞并进入等待集合。对消费者线程执行 take 时也是类似的:如有等待中的生产者,它们配对交接;如果没有生产者等待,则消费者线程阻塞进入等待集合。

    • 对于生产/消费并发控制,JDK 底层的实现对上面这种等待线程的管理分为两种模式:非公平模式下使用栈结构后进先出(LIFO)地管理等待线程(内部类称为TransferStack),公平模式下使用队列结构先进先出(FIFO)地管理等待线程(内部类TransferQueue)。

    • 对于协调生产者/消费者,没有像LinkedBlockingQueue使用锁机制,而是采用了 CAS 来管理。

  • 容量特性:容量为0,无法缓存任何元素。

  • 操作特性:

    • 严格同步,生产者和消费者必须成对出现:插入操作(put())必须等待对应删除操作(take()),反之亦然;

    • 不支持迭代和查看元素(如 peek() 永远返回 null)。

  • 适用场景:

    • 缓存线程池(如Executors.newCachedThreadPool())使用SynchronousQueue直接把任务交给线程或创建新线程执行;

    • 需要严格同步交接的场景(比如两个线程交替工作)。

    • 适合生产消费速率相当、要求低延迟无排队的场景

其在 JDK 实现的类UML 如下图:


一表式总结

根据上面的两个类的 UML 图,可以发现两者都实现了相同的接口BlockingQueue,所以都是阻塞队列,在特定条件下都会阻塞线程调用,只是底层实现不相同而已。

对于不相同的地方,下面的表格总结了 LinkedBlockingQueue 和 SynchronousQueue 在各方面的差异:

对比维度 LinkedBlockingQueue(LBQ) SynchronousQueue (SQ)
容量 可选有界/无界(默认)的FIFO阻塞队列,基于链表节点存储元素 容量为0的阻塞队列,不存储元素,只在线程间直接交换数据
底层结构 链表结构:内部有节点类存放元素,维护头尾指针和计数器。使用两把锁(putLock/takeLock)分别控制入队出队。有条件变量 notFull/notEmpty 用于阻塞等待。 无具体数据结构容器。JDK内部通过等待线程队列/栈管理:非公平模式用栈(LIFO),公平模式用队列(FIFO)存放等待的线程节点。通过 CASLockSupport 挂起/唤醒线程来交换元素。
线程交互 生产者插入操作在队列满时阻塞,消费者移除操作在队列空时阻塞;可以同时有多个元素在队列中等待处理。 每次插入操作必须等待有对应的移除操作才能进行,反之亦然。队列中始终不会有多于一个元素存在(实际上最多瞬间有一个正在交接的元素),生产和消费必须配对完成。
性能特点 插入和移除使用独立锁,支持一定程度并行,吞吐量高;在高并发下存在锁竞争和上下文切换,性能可能不够稳定。内有缓冲会增加任务延迟但减少生产者阻塞。 采用无锁算法,线程直接配对交换,极低的同步开销,单对线程下吞吐极高;无缓冲减少了排队延迟,但如果一方线程不足会使另一方阻塞等待。大量线程不匹配时可能出现许多线程挂起,极端高并发下吞吐可能下降。公平模式下性能略低于非公平模式,因为要额外开销保证 FIFO。
典型应用 固定大小线程池(如 Executors.newFixedThreadPool())使用无界LBQ存放多余任务;通用生产-消费者模型需要缓冲时。适合生产消费速率不一致、有突发流量需要缓冲的场景。 缓存线程池(如 Executors.newCachedThreadPool())使用SQ直接把任务交给线程或创建新线程执行;需要严格同步交接的场景。适合生产消费速率相当、要求低延迟无排队的场景。
公平策略 不支持 支持
迭代能力 提供弱一致性迭代 不支持迭代

如何快速理解两者的工作原理

为了能够快速理解两者的工作原理,这里以快递送达为比喻进行解释:

  • LinkedBlockingQueue 像是菜鸟物流,快递员(生产者 )总是把包裹(任务 )放到菜鸟驿站(里面有固定数量的储物架,可理解为任务队列 ),收件人(消费者 )可以在空闲时去菜鸟驿站取件,而不用必须等快递员把包裹送到面前,即强调双方的时间是可以错开的,包裹的送达(入队 )和领取(出队)的动作是可以异步进行的。

  • SynchronousQueue 就像是闪送,快递员(生产者 )必须把包裹(任务 )当面交给收件人(消费者 ),因没有储物架(没有任务队列)而不能提前送达:如果收件人没有来,快递员则会一直等待收件人出现,同理,收件人也只能等待快递员出现才能当面领到包裹,即强调双方必须同时在场。


以上。如有错误疏漏,欢迎评论一起探讨!


  • 如果你觉得我的工作对你有帮助,可以通过分享和推荐这篇文字或者关注同名公众号来支持我,你的支持是我持续创作的动力:

  • 转载以及引用请注明原文链接

  • 本博客所有文章除特别声明外,均采用CC 署名-非商业使用-相同方式共享 许可协议。

相关推荐
程序员小假18 分钟前
说一说 SpringBoot 中 CommandLineRunner
java·后端
sky_ph27 分钟前
JAVA-GC浅析(一)
java·后端
爱coding的橙子28 分钟前
每日算法刷题Day24 6.6:leetcode二分答案2道题,用时1h(下次计时20min没写出来直接看题解,节省时间)
java·算法·leetcode
岁忧33 分钟前
(nice!!!)(LeetCode每日一题)2434. 使用机器人打印字典序最小的字符串(贪心+栈)
java·c++·算法·leetcode·职场和发展·go
天天摸鱼的java工程师42 分钟前
@Autowired 注入失效?
java·后端
sss191s1 小时前
校招 Java 面试基础题目解析学习指南含新技术实操要点
java·python·面试
编程毕设1 小时前
【含文档+PPT+源码】基于微信小程序的旅游论坛系统的设计与实现
java·tomcat·旅游
爱吃土豆的马铃薯ㅤㅤㅤㅤㅤㅤㅤㅤㅤ1 小时前
saveOrUpdate 有个缺点,不会把值赋值为null,解决办法
java·开发语言
bytebeats1 小时前
Java 21 虚拟线程 - 兄嘚, 我的锁呢?
java
随缘而动,随遇而安1 小时前
第七十四篇 高并发场景下的Java并发容器:用生活案例讲透技术原理
java·大数据·后端