单并行度下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
差异原因解析
核心区别在于两种线程池的任务调度逻辑,尤其是线程处于等待状态时的处理机制:
ThreadPoolExecutor的行为
单线程的ThreadPoolExecutor执行第一个任务时,任务调用CompletableFuture.get()进入阻塞状态,此时工作线程会一直等待该Future完成,不会去调度队列中的第二个任务,直到当前任务完全执行完毕(包括Future的等待过程)。这是因为ThreadPoolExecutor的工作线程在执行任务时,不会主动“让出”线程去处理其他任务,即使当前任务处于阻塞。ForkJoinPool的行为
ForkJoinPool的核心设计是工作窃取(Work Stealing),即使并行度设为1,它的工作线程在遇到阻塞(比如调用Future.get())时,会触发线程的“托管”机制:工作线程会暂时挂起当前任务,转而从任务队列中取出下一个可执行的任务执行,直到原来的阻塞条件解除。
在你的代码中,第一个任务执行到doProcessing(itemVal).get()时进入阻塞,ForkJoinPool的工作线程会立即去执行第二个提交的任务,因此会先打印两个Started;等两个异步线程都完成后,再依次执行后续的Ended输出。
简单来说,ForkJoinPool的单线程会在任务阻塞时主动调度其他任务,而ThreadPoolExecutor的单线程会一直阻塞到当前任务完成,再处理下一个任务。
内容的提问来源于stack exchange,提问作者Rahul

