为何提交至ForkJoinPool公共池的任务有时会在主线程中执行?
这其实是ForkJoinPool和ForkJoinTask的内置特性,不是JIT编译器的优化,核心是ForkJoinTask的get()方法会触发外部线程协助执行任务的逻辑,具体拆解如下:
1. ForkJoinTask.get()的核心行为
当你调用ForkJoinTask.get()时,如果任务还处于未执行的状态,当前线程(这里就是主线程,属于ForkJoinPool外部的线程)不会单纯阻塞等待池内worker线程来执行任务,而是会尝试主动帮忙执行这个任务,以此减少线程上下文切换的开销,提升整体执行效率。
从你提供的主线程执行堆栈里也能看到关键证据:
at java.base/java.util.concurrent.ForkJoinTask.tryExternalHelp(ForkJoinTask.java:386) at java.base/java.util.concurrent.ForkJoinTask.externalInterruptibleAwaitDone(ForkJoinTask.java:356) at java.base/java.util.concurrent.ForkJoinTask.get(ForkJoinTask.java:1009)
tryExternalHelp方法就是专门用来让外部线程协助执行任务的入口。
2. 第一个循环的现象解释
在第一个循环里,你提交任务后立刻调用get(),此时ForkJoinPool的worker线程还没来得及拾取这个任务(毕竟线程调度需要时间),所以主线程就直接进入了"协助执行"的逻辑,自己把任务跑了,这就是为什么mainThreadCounter会有很高的数值。
3. 第二个循环的差异原因
第二个循环里,你在submit任务和get()之间加了一行:
ForkJoinPool.commonPool().submit(Thread::yield);
这行代码给了ForkJoinPool的worker线程一个"工作机会"——当你提交这个yield任务时,池内的worker线程会被唤醒并开始扫描任务队列,此时第一个任务很大概率已经被worker线程拾取并执行了。等你调用future.get()的时候,任务已经完成,主线程自然不需要再帮忙执行,所以mainThreadCounter变成了0。
4. 这不是bug,是设计特性
ForkJoinPool的核心设计思想是工作窃取(work-stealing),而外部线程在等待任务时协助执行,是这个设计的延伸——尽可能利用所有可用的线程资源来处理任务,避免不必要的阻塞和切换,这是官方有意为之的优化,完全符合预期。
内容的提问来源于stack exchange,提问作者mmirwaldt

