synchronousqueue不能存任务是因为其无容量设计,put/take必须配对阻塞完成直接移交,从而实现零延迟;它适合零延迟移交正因任务不缓冲、不拷贝,仅在线程间即时传递。

为什么 SynchronousQueue 不能存任务,反而适合零延迟移交
SynchronousQueue 的核心特性是它没有内部容量——offer() 只有在有另一个线程正调用 poll()(或 take())时才返回 true;否则立即失败或阻塞。这意味着任务不会“落盘”或暂存,而是必须由消费者线程当场接住。这正是零延迟移交的物理基础:任务对象构造完成、生产者调用 put() 的那一刻,就直接 handed off 到消费者线程的栈帧中,中间不经过队列缓冲。
注意:这不是“快”,而是“无中转”。只要消费者线程空闲并已进入阻塞等待,移交就是一次线程间直接引用传递,连对象拷贝都省了。
用 SynchronousQueue 构建线程池时,必须配 CallerRunsPolicy 或自定义拒绝策略
因为 SynchronousQueue 容量为 0,当所有核心线程忙、又没空闲线程可立即接手任务时,execute() 会直接触发拒绝策略——哪怕线程池还没到最大线程数。这是最容易踩的坑:开发者以为设了 maxPoolSize=10 就能撑住突发流量,结果发现刚超过核心数就开始拒绝。
- 默认
AbortPolicy抛RejectedExecutionException,线上几乎不可用 -
CallerRunsPolicy是最稳妥的选择:由提交任务的线程自己执行该任务,既避免丢失,又天然限流(调用方线程被占用,自然降低提交速率) - 若需保任务,必须配合外部缓冲(如内存队列 + 异步落盘),但那就不再满足“零延迟”前提了
必须用 Executors.newCachedThreadPool() 或手动构造,不能用 newFixedThreadPool
newFixedThreadPool(int n) 内部用的是 LinkedBlockingQueue,和 SynchronousQueue 无关;而 newCachedThreadPool() 才是标准搭配:corePoolSize=0、maxPoolSize=MAX_VALUE、keepAliveTime=60L、队列为 SynchronousQueue。手动构造时关键点:
new ThreadPoolExecutor(
0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<runnable>(),
new ThreadPoolExecutor.CallerRunsPolicy()
);</runnable>
这里 corePoolSize=0 很关键:它确保没有常驻线程,每个任务都触发新线程创建(或复用刚回收的),从而让 SynchronousQueue 的“即产即销”逻辑生效。设成大于 0 会导致部分任务被核心线程“吃掉”,反而引入排队等待。
零延迟只对“就绪消费者”成立,冷启动和线程回收期仍有延迟
所谓零延迟,仅发生在消费者线程已处于 take() 阻塞状态的前提下。实际中两个常见破环点:
- 线程刚创建/刚唤醒时有上下文切换开销,首次任务移交可能多几微秒
-
keepAliveTime后线程退出,下次任务来又要重建线程——这时移交变成“先创建线程,再传任务”,不再是零延迟 - 如果消费者处理逻辑本身慢(比如 IO 等待),那后续任务即使立刻移交,也会在消费者线程里排队,这不是队列导致的,但用户感知仍是“延迟”
所以真实场景下,“零延迟”是个理想边界条件,要稳定逼近它,得保证线程池长期有足够空闲线程常驻(比如用 prestartAllCoreThreads() 预热),且消费者逻辑尽量轻量。










