工作窃取通过空闲线程窃取其他线程双端队列尾部任务,避免CPU空转;LIFO本地执行+FIFO窃取降低锁竞争与伪共享;默认并行度为CPU核数减1以平衡调度开销;仅在任务可分、耗时不均时有效。
工作窃取为什么能减少CPU空转
当线程池里某些线程提前做完自己队列里的任务,而其他线程还在忙时,传统线程池(如
)会让空闲线程干等——CPU核心闲置。ForkJoinPool 不让这种情况发生:空闲线程会主动去“偷”别人队列尾部的任务执行,让所有核心持续有活干。
关键点在于“偷”的位置是队尾(
指针),而线程自己取任务是从队头(
指针)——这种 LIFO + FIFO 混合访问天然规避了锁竞争,也降低了缓存伪共享风险。
双端队列(Deque)不是为了存得多,而是为了偷得稳
每个
都维护一个自己的
,它不是普通队列:
本地执行:用
或
从头部取,保证最近 fork 的子任务优先执行(局部性好)
被窃取:其他线程用
从尾部取,拿走最早入队、可能已等待较久的任务
无锁设计:头部和尾部操作由不同线程主导,避免 CAS 争抢,吞吐更高
并行度设成 CPU 核心数减 1 是有道理的
默认并行度是
,通常等于
。这不是拍脑袋定的:
留一个核给主线程或系统调度,避免因抢占导致延迟毛刺
分治任务常有深度递归调用栈,过高的并行度反而增加上下文切换和队列管理开销
实测中,并行度 > CPU 核心数后,计算密集型任务性能常不升反降
任务太小或不可分割,工作窃取就成负优化
工作窃取只在任务天然可拆、子任务耗时不均时才真正起效。如果:
所有子任务都短于 10μs,窃取开销(定位队尾、跨缓存行读取)可能超过执行收益
任务之间有强 IO 依赖(比如每个子任务都要查一次 DB),那用
更合适
用了
却没重写
中的
/
逻辑,实际仍是单线程跑
真正容易被忽略的是:工作窃取不是万能负载均衡器,它不解决任务倾斜的根源——比如阈值(
)设得太大,导致大部分任务没被 fork 就直接算完了,窃取根本没机会发生。
ThreadPoolExecutorbasetopForkJoinWorkerThreadDequepoppollpollLastForkJoinPoolForkJoinPool.getCommonPoolParallelism()Runtime.getRuntime().availableProcessors() - 1ThreadPoolExecutorRecursiveActioncompute()fork()join()THRESHOLD