缘起

最近在 review 同事代码时,看到其使用了org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor 来构建线程池,而没有使用 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 署名-非商业使用-相同方式共享 许可协议。

对比分析LinkedBlockingQueue和SynchronousQueue的更多相关文章

  1. 浅谈C++之冒泡排序、希尔排序、快速排序、插入排序、堆排序、基数排序性能对比分析之后续补充说明(有图有真相)

    如果你觉得我的有些话有点唐突,你不理解可以想看看前一篇<C++之冒泡排序.希尔排序.快速排序.插入排序.堆排序.基数排序性能对比分析>. 这几天闲着没事就写了一篇<C++之冒泡排序. ...

  2. wait、notify、sleep、interrupt对比分析

    对比分析Java中的各个线程相关的wait().notify().sleep().interrupt()方法 方法简述 Thread类 sleep:暂停当前正在执行的线程:(类方法) yield:暂停 ...

  3. Android和Linux应用综合对比分析

    原文地址:http://www.cnblogs.com/beer/p/3325242.html 免责声明: 当时写完这篇调查报告,给同事看了后,他觉得蛮喜欢,然后想把这篇文章修改一下,然后往期刊上发表 ...

  4. GitHub & Bitbucket & GitLab & Coding 的对比分析

    目前基于 Git 做版本控制的代码托管平台有很多种,比较流行的服务有 Github.Bitbucket. GitLab. Coding,他们各自有什么特点,个人使用者和开发团队又该如何选择? 在这篇文 ...

  5. ArrayList和LinkedList的几种循环遍历方式及性能对比分析(转)

    主要介绍ArrayList和LinkedList这两种list的五种循环遍历方式,各种方式的性能测试对比,根据ArrayList和LinkedList的源码实现分析性能结果,总结结论. 通过本文你可以 ...

  6. ArrayList和LinkedList的几种循环遍历方式及性能对比分析

    最新最准确内容建议直接访问原文:ArrayList和LinkedList的几种循环遍历方式及性能对比分析 主要介绍ArrayList和LinkedList这两种list的五种循环遍历方式,各种方式的性 ...

  7. ArrayList和LinkedList的几种循环遍历方式及性能对比分析(转载)

    原文地址: http://www.trinea.cn/android/arraylist-linkedlist-loop-performance/ 原文地址: http://www.trinea.cn ...

  8. ArrayList和LinkedList遍历方式及性能对比分析

    ArrayList和LinkedList的几种循环遍历方式及性能对比分析 主要介绍ArrayList和LinkedList这两种list的五种循环遍历方式,各种方式的性能测试对比,根据ArrayLis ...

  9. 【产品对比分析】See做了明星衣橱想做的东西?

    不断地发现.联想.思考,让学到的东西互通起来吧!  先来两张See的界面图镇楼——          See简介: See是一个专注找同款的时尚社区,主打功能是一键拍照找同款,由社区为你提供最佳商品或 ...

  10. 【转】ArrayList和LinkedList的几种循环遍历方式及性能对比分析

    原文网址:http://www.trinea.cn/android/arraylist-linkedlist-loop-performance/ 主要介绍ArrayList和LinkedList这两种 ...

随机推荐

  1. Vue2框架-基础

    1. vue简介 什么是vue? Vue是一套用于构建用户界面的渐进式JavaScript框架.与其它大型框架不同的是,Vue 被设计为可以自底向上逐层应用.Vue 的核心库只关注视图层,方便与第三方 ...

  2. windows goland go exec "gcc": executable file not found in %PATH%

    问题 windows 本地缺少 gcc 编译器 解决方案 下载安装使用 MinGW-w64 第一种 https://winlibs.com/#download-release 下载后解压到磁盘中,然后 ...

  3. Java List和Array之间的转换

    import java.util.Arrays; import java.util.List; class Test { //Object数组向List的转换 public static List&l ...

  4. 通过局域网访问连接 vite 或 Django 之类的项目

    博客地址:https://www.cnblogs.com/zylyehuo/ step1 将 vite 或 Django 类的项目启动 ip 设置为 0.0.0.0:端口 step2 查询本机电脑在当 ...

  5. Docker Swarm 进阶:集群容错

  6. Redis 原理 - List

    List 数据结构 Redis 3.2 前,使用 压缩列表zipList 或 双向链表linkedList 当同时满足下面两个条件时,使用zipList存储数据 list保存的每个元素长度小于64字节 ...

  7. Netty源码—6.ByteBuf原理一

    大纲 1.关于ByteBuf的问题整理 2.ByteBuf结构以及重要API 3.ByteBuf的分类 4.ByteBuf分类的补充说明 5.ByteBuf的主要内容分三大方面 6.内存分配器Byte ...

  8. JDBC-增删查改操作

    使用场景:测试家族族长分成时需要批量添加家族流水记录,但手动添加和SQL语句添加较为麻烦 操作步骤 运行环境:Java8+IDEA 1.打开IDEA 点击File->New->Projec ...

  9. 树状数组(Fenwick Tree)原理和优化全面解析

    你正在开发一个交易系统,需要实时完成两种操作: 更新某个时间点的价格(单点修改) 快速计算某段时间段内的交易总量(区间查询) 当数据量较小时,我们可能会这样实现: vector<int> ...

  10. centos firewall防火墙操作指令记录

    1. 查看防火墙状态 systemctl status firewalld.service 2. 关闭防火墙 systemctl stop firewalld.service 3. 开机自动关闭防火墙 ...