countedcompleter专用于有依赖关系的并行子任务协调,如分段哈希校验、树形聚合等;需用setpendingcount预设等待数、子任务完成调complete/trycomplete、合并逻辑必须在oncompletion中实现,compute不返回值且不可阻塞等待。

CountedCompleter 不是给普通递归用的,它是为“有依赖关系的并行子任务”设计的底层协调工具;直接拿它改写简单递归,大概率掉进线程饥饿、计数错乱、完成逻辑失控的坑里。
什么时候非得用 CountedCompleter?
典型场景:一个任务必须等多个子任务都产出中间结果后,才能合并计算(比如分段哈希校验、树形结构的自底向上聚合、带前置条件的图遍历)。ForkJoinTask 的默认模型(RecursiveAction/RecursiveTask)没法自然表达“我等 N 个孩子完成再干活”,而 CountedCompleter 的 setPendingCount 和 tryComplete 就是干这个的。
常见错误现象:onCompletion 没触发、任务卡在 tryComplete 后不继续、compute 被重复调用。
- 必须重写
onCompletion处理合并逻辑,不能只靠compute -
setPendingCount(n)要在 fork 子任务前调用,且n必须等于实际 fork 出的子任务数 - 每个子任务完成后必须调用
complete(null)或tryComplete(),否则父任务永远等不到
compute() 里 fork 子任务的写法陷阱
和 RecursiveTask 不同,CountedCompleter 的 compute() 不返回值,结果要靠副作用或共享状态传递。容易忽略的关键点:
立即学习“Java免费学习笔记(深入)”;
- 不能在
compute()末尾 return 一个值——它签名是void - 子任务必须用
fork()启动,不能直接compute()调用(否则失去并行性,且计数机制失效) - 如果某层不需要并行(比如数据量小),应调用
quietlyComplete()主动标记完成,而不是漏掉tryComplete
示例片段(求数组区间和,但要求左右子区间和都算完才加总):
void compute() {
if (hi - lo <= THRESHOLD) {
sum = IntStream.range(lo, hi).mapToLong(i -> arr[i]).sum();
quietlyComplete(); // 主动完成,不等别人
return;
}
int mid = (lo + hi) / 2;
setPendingCount(2); // 明确等两个子任务
new SumTask(arr, lo, mid).fork();
new SumTask(arr, mid, hi).fork();
// 不 return —— 等 onCompletion 触发
}为什么 onCompletion 比 compute 更关键?
onCompletion 是唯一能安全访问所有子任务结果的地方,也是整个依赖链的“汇合点”。很多人误以为在 compute 里等子任务就行,但那是阻塞式等待,会拖垮 ForkJoinPool 的工作线程。
-
onCompletion参数是当前任务自身(CountedCompleter实例),不是子任务结果——结果得你自己存(比如用volatile long sum字段) - 子任务的结果不能通过返回值传上来,必须用共享字段 + 内存可见性保障(
volatile或AtomicLong) - 如果子任务抛异常,
onCompletion仍会执行,需配合getRawResult()或额外标志位判断是否成功
真正难的不是写对语法,是想清楚“谁等谁”“谁改什么”“谁看什么”。哪怕只多一层依赖,计数逻辑就可能从线性变网状;一旦漏掉一次 tryComplete 或错设 pendingCount,整个任务树就静默卡死——这种问题在线程堆栈里根本看不出线索。









