缘起
最近在 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,这种锁分离机制,可以使生产者的入队操作和消费者的出队操作可以并行。
-
同时,为了协调生产者/消费者,其还配备了对应的条件变量:在队列满时阻塞生产者的
notFull
(notFull=putLock.newCondition()
),以及在队列空时阻塞消费者的notEmpty
(notEmpty=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)存放等待的线程节点。通过 CAS 和 LockSupport 挂起/唤醒线程来交换元素。 |
线程交互 | 生产者插入操作在队列满时阻塞,消费者移除操作在队列空时阻塞;可以同时有多个元素在队列中等待处理。 | 每次插入操作必须等待有对应的移除操作才能进行,反之亦然。队列中始终不会有多于一个元素存在(实际上最多瞬间有一个正在交接的元素),生产和消费必须配对完成。 |
性能特点 | 插入和移除使用独立锁,支持一定程度并行,吞吐量高;在高并发下存在锁竞争和上下文切换,性能可能不够稳定。内有缓冲会增加任务延迟但减少生产者阻塞。 | 采用无锁算法,线程直接配对交换,极低的同步开销,单对线程下吞吐极高;无缓冲减少了排队延迟,但如果一方线程不足会使另一方阻塞等待。大量线程不匹配时可能出现许多线程挂起,极端高并发下吞吐可能下降。公平模式下性能略低于非公平模式,因为要额外开销保证 FIFO。 |
典型应用 | 固定大小线程池(如 Executors.newFixedThreadPool() )使用无界LBQ存放多余任务;通用生产-消费者模型需要缓冲时。适合生产消费速率不一致、有突发流量需要缓冲的场景。 |
缓存线程池(如 Executors.newCachedThreadPool() )使用SQ直接把任务交给线程或创建新线程执行;需要严格同步交接的场景。适合生产消费速率相当、要求低延迟无排队的场景。 |
公平策略 | 不支持 | 支持 |
迭代能力 | 提供弱一致性迭代 | 不支持迭代 |
如何快速理解两者的工作原理
为了能够快速理解两者的工作原理,这里以快递送达为比喻进行解释:
-
LinkedBlockingQueue
像是菜鸟物流,快递员(生产者 )总是把包裹(任务 )放到菜鸟驿站(里面有固定数量的储物架,可理解为任务队列 ),收件人(消费者 )可以在空闲时去菜鸟驿站取件,而不用必须等快递员把包裹送到面前,即强调双方的时间是可以错开的,包裹的送达(入队 )和领取(出队)的动作是可以异步进行的。 -
SynchronousQueue
就像是闪送,快递员(生产者 )必须把包裹(任务 )当面交给收件人(消费者 ),因没有储物架(没有任务队列)而不能提前送达:如果收件人没有来,快递员则会一直等待收件人出现,同理,收件人也只能等待快递员出现才能当面领到包裹,即强调双方必须同时在场。
以上。如有错误疏漏,欢迎评论一起探讨!
-
如果你觉得我的工作对你有帮助,可以通过分享和推荐这篇文字或者关注同名公众号来支持我,你的支持是我持续创作的动力:
-
转载以及引用请注明原文链接。
-
本博客所有文章除特别声明外,均采用CC 署名-非商业使用-相同方式共享 许可协议。