You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

单并行度下ForkJoinPool与ThreadPoolExecutor行为差异原因探究

ForkJoinPool与ThreadPoolExecutor单并行度下的行为差异解析

问题场景

将ThreadPoolExecutor(单线程配置)和ForkJoinPool(并行度设为1)分别执行两个任务,预期两者都应串行完成任务,但实际输出存在明显差异。

示例代码

public class Sample {

public static void main(String[] args) {
    // Use either ThreadPoolExecutor or ForkJoin.
    //ThreadPoolExecutor pool = (ThreadPoolExecutor) Executors.newFixedThreadPool(1);
    ForkJoinPool pool = new ForkJoinPool(1);
    List<Integer> items = new ArrayList<>();
    items.add(1);
    items.add(2);
    // Create 2 task and submit to thread pool. Wait for it to execute all the task.
    pool.submit(() -> {
        try {
            startProcessing(items.get(0));
        } catch (ExecutionException e) {
            e.printStackTrace();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    });
    pool.submit(() -> {
        try {
            startProcessing(items.get(1));
        } catch (ExecutionException e) {
            e.printStackTrace();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    });


    try {
        pool.shutdown();
        pool.awaitTermination(10, TimeUnit.SECONDS);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}
private static void startProcessing(int itemVal) throws ExecutionException, InterruptedException {
    doProcessing(itemVal).get();
    System.out.println("Ended :" + itemVal);
}

private static CompletableFuture<Integer> doProcessing(int itemVal) throws InterruptedException {
    CompletableFuture<Integer> future = new CompletableFuture<>();
    System.out.println("Started :" + itemVal);
    // I was assuming as this is waiting for child thread to also complete,
    // so this thread will be waiting and hence will be waiting for task 1 untill it finishes.
    // BUT IT GOT CONTEXT SWITCHED AND PICKED THE TASK 2.
    asyncCall(future, itemVal);
    return future;
}

// This is the place where new thread is created.
private static void asyncCall(CompletableFuture<Integer> future, int itemVal) {
    Thread t = new Thread(() -> {
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        future.complete(itemVal);
    });
    t.start();
}
}

输出对比

使用ThreadPoolExecutor时的输出

Started :1
Ended :1
Started :2
Ended :2

使用ForkJoinPool时的输出

Started :1
Started :2
Ended :1
Ended :2

差异原因解析

核心区别在于两种线程池的任务调度逻辑,尤其是线程处于等待状态时的处理机制:

  1. ThreadPoolExecutor的行为
    单线程的ThreadPoolExecutor执行第一个任务时,任务调用CompletableFuture.get()进入阻塞状态,此时工作线程会一直等待该Future完成,不会去调度队列中的第二个任务,直到当前任务完全执行完毕(包括Future的等待过程)。这是因为ThreadPoolExecutor的工作线程在执行任务时,不会主动“让出”线程去处理其他任务,即使当前任务处于阻塞。

  2. ForkJoinPool的行为
    ForkJoinPool的核心设计是工作窃取(Work Stealing),即使并行度设为1,它的工作线程在遇到阻塞(比如调用Future.get())时,会触发线程的“托管”机制:工作线程会暂时挂起当前任务,转而从任务队列中取出下一个可执行的任务执行,直到原来的阻塞条件解除。

在你的代码中,第一个任务执行到doProcessing(itemVal).get()时进入阻塞,ForkJoinPool的工作线程会立即去执行第二个提交的任务,因此会先打印两个Started;等两个异步线程都完成后,再依次执行后续的Ended输出。

简单来说,ForkJoinPool的单线程会在任务阻塞时主动调度其他任务,而ThreadPoolExecutor的单线程会一直阻塞到当前任务完成,再处理下一个任务。


内容的提问来源于stack exchange,提问作者Rahul

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.07 08:45:15