newcachedthreadpool 必须用 synchronousqueue,因其无缓冲、强移交语义:任务要么被空闲线程立即取走,要么触发新建线程,绝不排队;size()恒为0、offer()失败即扩容,是设计契约而非bug。

为什么 newCachedThreadPool 一定要用 SynchronousQueue
因为线程池要实现“任务来了,有空闲线程就立刻交过去;没有,就新建线程”,中间不能有排队缓冲——而 SynchronousQueue 正是唯一能强制做到这点的队列。它不存任务,offer() 不成功就立即返回 false,put() 则必须等到工作线程调用 take() 才能返回,这天然匹配 CachedThreadPool 的“无队列、只创建”语义。
常见错误是误以为它像 LinkedBlockingQueue 那样“先塞进去再慢慢消费”。实际上,一旦你把任务提交给用了 SynchronousQueue 的线程池,这个任务要么被某个空闲线程当场接走(take() 返回),要么触发线程池扩容逻辑(execute() 内部判断 offer() 失败后走 addWorker())。中间不存在“暂存”状态。
-
Executors.newCachedThreadPool()内部构造的是new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue<runnable>())</runnable> - 核心线程数为 0,意味着所有任务都得靠
SynchronousQueue的移交能力来触发新线程创建 - 如果换成
ArrayBlockingQueue或无界LinkedBlockingQueue,任务会堆积在队列里,线程数永远涨不起来,背压就藏在内存里了
size() == 0 不是 bug,是设计契约
SynchronousQueue.size() 永远返回 0,isEmpty() 永远返回 true,这不是实现缺陷,而是它“不存储”的直接体现。你看到监控里队列长度恒为 0,别急着改配置——它本就不该有长度。
调试时若发现任务卡住,问题一定不在“队列满了”,而在于:有没有空闲线程正在调用 take()?或者线程是否被阻塞在其他地方(比如 I/O、锁、GC)导致无法响应?
-
peek()和iterator()直接抛UnsupportedOperationException,别试图检查内容 -
remainingCapacity()总是返回0,别拿它做容量预判 - 日志或指标中显示
queue.size() == 0是健康态,不是告警信号
offer() vs put():移交意图决定行为差异
两者表面都是“插入”,但语义完全不同:put() 是强同步移交(必须等到消费者),offer() 是尽力移交(没人在等就立刻失败)。在线程池内部,execute() 方法先调 offer() 尝试交给空闲线程;失败才新建线程。这个“失败”不是异常,而是正常控制流分支。
-
queue.offer(task):无等待线程时返回false,不阻塞,适合做快速路径判断 -
queue.put(task):一定会阻塞到有线程take(),用于必须确保任务被接收的场景(如信号量式协调) -
queue.offer(task, 0, TimeUnit.NANOSECONDS)等价于无参offer(),但显式写出更清晰 - 千万别写
queue.offer(task) || fallbackToDisk()期望“队列满才兜底”——SynchronousQueue永远不满,它只看“此刻有没有人等着接”
公平模式(TransferQueue)和非公平模式(TransferStack)怎么选
默认非公平(new SynchronousQueue())用的是 TransferStack,新来的 put() 更容易匹配刚 take() 完正自旋的线程,吞吐略高;公平模式(new SynchronousQueue(true))用 TransferQueue,严格 FIFO,先等的先被服务。
对线程池来说,newCachedThreadPool 用非公平是合理选择:任务本身无序,快比稳重要;但如果你在做事件总线、信号分发这类要求顺序交付的场景,就得显式传 true。
- 非公平模式下,
put()可能跳过前面等待的线程,直接匹配后到的活跃线程,压测吞吐高约 5–10% - 公平模式多一次 CAS + 队列维护开销,但避免饥饿,适合低延迟敏感且需确定性调度的系统
- 两种模式都不影响
size()和isEmpty()的返回值——它们都恒为 0 / true
size() 或想遍历它,就已经偏离了它的使用前提。










