大家好,我是妍妍不写代码。
今天跟大伙聊一个 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