recursiveaction 是 forkjoinpool 专用的无返回值递归任务抽象类,需继承重写 compute();与 runnable 不同,它支持自动拆分、工作窃取,但必须运行于 forkjoinpool 中,且依赖合理阈值与 fork/join 配合。

RecursiveAction 是什么,和普通 Runnable 有啥区别
它就是 ForkJoinPool 里专干“不返回结果”的并行活的抽象类,不是接口,不能直接 new,得继承重写 compute()。和 Runnable 表面看着像(都没返回值),但底层完全两码事:Runnable 丢给线程池就完事,RecursiveAction 能自动拆任务、合并子任务、利用工作窃取——前提是你的任务真能递归切分。
- 必须跑在
ForkJoinPool里,扔给Executors.newCachedThreadPool()会静默失败或抛IllegalThreadStateException -
compute()里不能用return传结果,只能改成员变量或外部容器(比如AtomicInteger或ConcurrentHashMap) - 没切分逻辑硬写
fork()+join()就是自找死路——任务不会变快,反而因调度开销更慢
什么时候该用 RecursiveAction,而不是 CompletableFuture 或 Stream.parallel()
适合“可自然分治 + 无依赖 + 侧重吞吐”的场景,比如遍历大数组做纯计算、批量解析日志行、图像分块处理。不适合有强顺序、频繁 I/O、或需要链式回调的活。
- 数据量小(比如 Stream.parallel() 更省心,
RecursiveAction启动成本高 - 要等中间结果再继续:别硬套,改用
CompletableFuture.supplyAsync()链式组合 - 任务之间要共享锁或状态:
RecursiveAction的线程不可控,容易死锁,老老实实用synchronized或ReentrantLock包住临界区
compute() 里 fork/join 的典型写法和常见崩法
核心就一条:拆到阈值以下才直接算,否则 fork() 子任务,再 join() 等结果。崩点全在边界判断和调用时机上。
- 忘记
if (end - start 直接递归:栈溢出或任务爆炸,<code>ForkJoinPool可能 OOM -
fork()后没join():子任务被扔出去就不管了,父任务提前结束,结果丢失 - 把
join()放在fork()前:串行执行,彻底失去并行意义 - 示例正确姿势:
protected void compute() { if (end - start <= 1000) { processRange(start, end); return; } int mid = (start + end) >>> 1; RecursiveAction left = new MyAction(data, start, mid); RecursiveAction right = new MyAction(data, mid, end); left.fork(); // 先 fork 左 right.compute(); // 右直接算(避免再 fork) left.join(); // 再等左 }
ForkJoinPool.commonPool() 和自建池的关键差异
默认的 ForkJoinPool.commonPool() 是 JVM 全局共享的,大小由 java.util.concurrent.ForkJoinPool.common.parallelism 控制(通常等于 CPU 核数减一)。你自己的任务混在里面,可能被其他框架(比如 Stream.parallel()、CompletableFuture)抢占资源。
- CPU 密集型任务多且重要:显式创建
new ForkJoinPool(4),避免被挤占 - 池子太小(比如设成 1):退化成单线程,还多一层调度开销
- 池子太大(比如设成 100):线程上下文切换压垮性能,
ForkJoinPool不是越大越好 - 别在
compute()里调shutdown():会直接杀掉整个池,连带其他正在跑的任务
实际用的时候,最常被忽略的是阈值设定——它不是拍脑袋定的,得结合数据特征和机器缓存行大小测;还有就是误以为 fork 了就等于并行执行了,其实没 join 就不算真正参与协作。










