答案要点:
ForkJoinPool 是 Java 7 引入的线程池,专为 分治任务 设计,支持递归任务拆分(Fork)和结果合并(Join)。
核心区别:
工作窃取算法(Work-Stealing):空闲线程从其他线程的任务队列尾部窃取任务,提高 CPU 利用率
任务拆分与合并:天然支持递归任务的并行化,而普通线程池需要手动拆分任务
适用场景:ForkJoinPool 适合 CPU 密集型任务(如并行计算),普通线程池适合通用异步任务
答案要点:
每个线程维护一个双端队列(Deque),存放自己的任务
当线程的任务队列为空时,会从其他线程的队列 尾部 窃取任务
高效原因:
减少线程空闲时间,最大化 CPU 利用率。
任务拆分和窃取是动态的,适合处理不均衡的任务负载
答案要点:
优点:
工作窃取算法适合处理大量短小的异步任务
公共线程池复用,避免频繁创建/销毁线程的开销
缺点:
公共线程池是全局共享的,如果任务阻塞(如 I/O),可能影响其他依赖公共池的任务
默认并行级别可能不适合所有场景(需根据 CPU 核心数调整)
改进方案:为 CompletableFuture
指定自定义线程池:
ExecutorService customPool = Executors.newFixedThreadPool(10);
CompletableFuture.runAsync(() -> {...}, customPool);
答案要点:
影响:
所有依赖公共线程池的功能(如并行流、CompletableFuture)性能下降
可能导致任务排队积压,甚至线程饥饿
避免方法:
禁止在公共线程池中执行阻塞操作(如同步 I/O)
为阻塞任务分配独立的线程池
1. ForkJoinPool的任务会被内部存储了一个WorkQueue数组,提交给ForkJoinPool的任务会被分配到指定的WorkQueue上执行
2. 每个WorkQueue内部维护了一个ForkJoinTask数组用来存储待执行的任务,以及一个独立的ForkJoinWorkerThread用来真正执行任务
以submit方法为例,我们看下ForkJoinPool
是如何处理提交的任务的,真正处理的方法是externalPush
,如下所示,
1. 判断工作队列数组是否为空
2. 通过按位与得到当前任务分配的工作队列
3. 尝试通过CAS锁定该队列
4. 判断工作队列中的任务数组是否为空
5. 释放锁
ForkJoinPool执行任务的过程如下
runWork()
1. 判断工作队列是否需要扩容
2. 阻塞调用对应WorkQueue的runTask方法
3. 如果无法获取到执行资源,就等待任务调用
1. 标记当前状态为忙碌
2. 调用当前任务执行
3. 继续执行当前队列中等待执行的任务
4. 尝试从其他WorkQueue中窃取任务执行
ForkJoinPool通过内部维护WorkQueue数组的方式,将ForkJoinTask任务分配到指定的工作队列中执行,通过将鸡蛋分散在多个篮子
这种分治思想提升线程的工作效率,并且针对线程执行过程中出现的空闲情况,设计出了工作窃取的算法,促使工作线程尽可能的饱和执行,在线程安全方面,通过Unsafe的CAS操作配合原子类,保证了多线程场景下的竞争安全问题
默认的ForkJoinPool.commonPool()是全局共享的,如果任务中包含阻塞操作(如I/O、同步锁、数据库查询等),可能导致公共线程池中的线程被长时间占用。这会降低系统的吞吐量,甚至引发线程饥饿(所有线程被阻塞,后续任务无法执行)
若所有类似任务都使用默认线程池,大量阻塞操作会迅速耗尽公共线程池的线程
CPU密集型任务和IO密集型任务
IO密集型任务不适合ForkJoinPool使用,因为它会长时间占用线程
ForkJoinPool在任务分治和CPU密集型场景中表现出色
ForkJoinPool.commonPool()的默认配置可能不适用于所有场景