大家好,我是妍妍不写代码。
今天跟大伙聊一个 Java 并发编程里看着很厉害、用起来却容易掉坑的家伙——ForkJoinPool。
很多朋友一听到“工作窃取”“分而治之”这些词,就觉得它特别智能。但等你真拿它跑任务,尤其是任务之间还有相互等待的时候,就会发现:它不但没帮你提速,反而把整个线程池给“卡死”了。
这不是它不聪明,而是它天生不适合某些场景。下面我就用大白话,把这个“陷阱”拆清楚。
一、先简单说下 ForkJoinPool 是干啥的
你可以把它理解成一个自带“偷活”能力的线程池。
每个线程有自己的任务队列,自己的活干完了,会去别的线程队列“偷”一个任务来干。这样理论上能最大化利用 CPU。
代码写起来也很“优雅”:
// 代码块1:一个简单的 ForkJoin 任务示例
public class SimpleTask extends RecursiveTask<Integer> {
private int start, end;
public SimpleTask(int start, int end) {
this.start = start;
this.end = end;
}
@Override
protected Integer compute() {
if (end - start <= 5) {
int sum = 0;
for (int i = start; i <= end; i++) {
sum += i;
}
return sum;
}
int mid = (start + end) / 2;
SimpleTask left = new SimpleTask(start, mid);
SimpleTask right = new SimpleTask(mid + 1, end);
left.fork();
return right.compute() + left.join();
}
}看着没问题对吧?别急,坑在后面。
二、真正的陷阱:任务互相等待
ForkJoinPool 有一个核心特点:它默认假设你的任务是独立的,不会互相阻塞。
但现实中的业务,经常是任务 A 等任务 B,任务 B 等任务 C。这时候问题就来了:
一旦某个任务在等待另一个任务的结果,当前线程不会去“偷”别的任务,而是傻傻地等。 这就把整个池子的并行能力给锁死了。
看下面这个反例:
// 代码块2:错误示范——任务互相等待
public class BlockingTask extends RecursiveAction {
private ForkJoinPool pool;
public BlockingTask(ForkJoinPool pool) {
this.pool = pool;
}
@Override
protected void compute() {
// 错误:在 ForkJoinTask 中直接提交新任务并等待
var future = pool.submit(() -> {
Thread.sleep(100);
return "result";
});
try {
// 这里会阻塞当前工作线程
String result = future.get();
System.out.println(result);
} catch (Exception e) {
e.printStackTrace();
}
}
}你跑一下就会发现:线程池很快就“卡死”了,后续任务再也无法执行。
三、为什么它会卡?一句话讲透
因为 ForkJoinPool 的工作线程数固定(默认是 CPU 核数)。一旦所有线程都在等某个结果,而这个结果又被排在队列后面没人执行,就形成了循环等待。
它不是不智能,而是它压根没为“任务互相等待”这种场景做优化。
// 代码块3:模拟卡死现象
public class DeadSimulation {
public static void main(String[] args) {
ForkJoinPool pool = new ForkJoinPool(2);
for (int i = 0; i < 4; i++) {
pool.submit(() -> {
System.out.println(Thread.currentThread() + " 开始");
// 模拟等待外部结果
Thread.sleep(5000);
System.out.println(Thread.currentThread() + " 结束");
return null;
});
}
// 主线程不结束,观察卡死现象
pool.shutdown();
}
}你会发现:只有两个线程在工作,另外两个任务永远在队列里等着。
四、什么场景容易掉进这个陷阱?
你在 ForkJoinTask 里调用了 Future.get()、CountDownLatch.await()、CompletableFuture.join()
你在任务里用了同步锁 synchronized 或 ReentrantLock
你在任务里调用了远程接口、数据库查询并同步等待
只要有一个任务会让当前线程停下来等,ForkJoinPool 就很容易“翻车”。
// 代码块4:危险操作示例——同步等待
public class DangerousTask extends RecursiveTask<String> {
@Override
protected String compute() {
// 危险:同步等待外部服务
return callRemoteService();
}
private String callRemoteService() {
try {
// 模拟网络请求
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
return "remote result";
}
}这种写法在普通线程池没问题,在 ForkJoinPool 里就可能拖垮整个池子。
五、怎么避免?三招搞定
第一招:不要混用阻塞操作
能用 CompletableFuture 异步就别用 get() 同步等。
// 代码块5:推荐做法——用异步替代同步
public class GoodTask extends RecursiveTask<CompletableFuture<String>> {
@Override
protected CompletableFuture<String> compute() {
return CompletableFuture.supplyAsync(() -> {
// 异步执行
return "result";
});
}
}第二招:专用线程池做隔离
把可能阻塞的任务交给普通线程池,ForkJoinPool 只做纯计算。
// 代码块6:隔离策略
ExecutorService blockingPool = Executors.newCachedThreadPool();
ForkJoinPool forkJoinPool = new ForkJoinPool();
forkJoinPool.submit(() -> {
blockingPool.submit(() -> {
// 阻塞操作放这里
Thread.sleep(1000);
});
});第三招:用 ManagedBlocker 接口
这是 ForkJoinPool 官方提供的补救方法,用来告诉线程池“我要阻塞了,你赶紧补一个线程”。
// 代码块7:官方补救方案——ManagedBlocker
public class CustomBlocker implements ForkJoinPool.ManagedBlocker {
private boolean done = false;
@Override
public boolean block() {
// 模拟阻塞操作
try {
Thread.sleep(100);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
done = true;
return true;
}
@Override
public boolean isReleasable() {
return done;
}
}
// 使用方式
ForkJoinPool.managedBlock(new CustomBlocker());但这个写法比较麻烦,日常开发用得不多。
六、问答环节
问1:那 ForkJoinPool 还能不能用?
答: 当然能用,但要用对地方。它最适合计算密集型、任务之间不互相等待的场景,比如:
大数组排序
递归计算(斐波那契、矩阵运算)
图像处理、数据聚合
如果你做的是 Web 服务、IO 密集型、任务有依赖 的场景,请老老实实用普通线程池。
问2:我已经用了 ForkJoinPool 并且卡死了,怎么快速定位?
答: 两步走:
用 jstack 看线程堆栈,找 java.util.concurrent.ForkJoinPool 相关的线程
看是否有大量线程处于 WAITING 或 TIMED_WAITING 状态,并且栈顶是 Future.get() 或 join()
// 代码块8:快速排查示例
public class QuickCheck {
public static void main(String[] args) {
ForkJoinPool pool = new ForkJoinPool(2);
pool.submit(() -> {
// 可疑的等待代码
Thread.currentThread().join();
return null;
});
// 然后执行 jstack 查看
}
}找到了就按上面“三招”去改。
七、总结一句人话
ForkJoinPool 是个优秀的“计算工人”,但你硬让它去“等人办事”,它就会罢工。
它不是不智能,而是你把它用错了场景。记住:
纯计算 ✅ 用它
IO 或任务等待 ❌ 别用它
希望这篇大白话能帮到正在被 ForkJoinPool 折磨的你。
我是妍妍不写代码,下期见。
更多经验分享:
https://segmentfault.com/a/1190000047812147
https://www.787game.com.cn/chaobian/77.html
https://www.687game.com.cn/hyfl/141.html
https://segmentfault.com/a/1190000047832038
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用。你还可以使用@来通知其他用户。