ForkJoinPool
ForkJoinPool
是 Java 并发包中一个非常核心且强大的组件,专门用于执行 ForkJoinTask
任务。它的设计精髓在于“分而治之”(Divide and Conquer)思想和“工作窃取”(Work-Stealing)算法。
ForkJoinPool 的核心概念
ForkJoinPool
是一个特殊的线程池,它与 ThreadPoolExecutor
的主要区别在于它使用了 工作窃取 机制。
a. 核心思想:分而治之 (Divide and Conquer)
ForkJoinPool
的设计初衷是为了解决那些可以被递归分解成更小子任务的问题。
- Fork (分解):一个大任务可以被分解(fork)成若干个独立的子任务。这个过程可以递归地进行,直到子任务足够小,可以直接计算出结果,不再需要进一步分解。
- Join (合并):子任务执行完毕后,它们的结果可以被合并(join)起来,最终汇集成大任务的结果。
ForkJoinTask
是这个模型中任务的抽象。它有两个主要的子类:
RecursiveAction
: 用于没有返回值的任务。RecursiveTask<V>
: 用于有返回值的任务。
b. 关键机制:工作窃取 (Work-Stealing)
这是 ForkJoinPool
高性能的关键。在一个传统的线程池中,所有线程都从一个公共的任务队列中获取任务。而在 ForkJoinPool
中,每个工作线程(ForkJoinWorkerThread
)都有自己的一个双端队列(Deque)来存放任务。
- LIFO (后进先出): 工作线程从自己队列的 头部 获取任务来执行。这利用了CPU缓存的优势,因为刚被分解出的子任务(热点数据)最有可能还在缓存中。
- FIFO (先进先出): 当一个线程完成了自己队列中的所有任务后,它会变得空闲。此时,它会去 随机 选择另一个线程,并从那个线程队列的 尾部 “窃取”一个任务来执行。窃取尾部的任务是为了减少与原线程的竞争,并且通常尾部的任务是较早创建的,可能是个更大的任务单元,有助于窃取者保持忙碌。
这种机制使得 CPU 资源得到充分利用,当任务分配不均时,空闲的线程可以主动去帮助繁忙的线程,从而实现负载均衡。
如何使用 ForkJoinPool
1. 获取线程池实例
-
使用公共池
commonPool()
: 对于大多数应用场景,直接使用ForkJoinPool.commonPool()
是最简单和推荐的方式。这个静态的公共池在整个 JVM 中共享,其大小默认为Runtime.getRuntime().availableProcessors() - 1
(在某些情况下会保证至少为1)。Java 8 的parallelStream()
内部就是使用的这个公共池。ForkJoinPool commonPool = ForkJoinPool.commonPool();
-
创建自定义池: 你也可以根据需要创建自定义的
ForkJoinPool
实例,指定并行度(即线程数)。// 创建一个包含4个线程的ForkJoinPool ForkJoinPool customPool = new ForkJoinPool(4);
2. 提交任务
ForkJoinPool
提供了多种提交任务的方式,主要分为从“外部”提交和在“内部”(即 ForkJoinTask
中)提交。
-
外部提交 (主要方法):
invoke(ForkJoinTask<T> task)
: 提交任务并 阻塞 当前线程,直到任务执行完成并返回结果。execute(ForkJoinTask<?> task)
: 异步执行任务,不返回结果,也无法获取任务状态。submit(ForkJoinTask<T> task)
: 异步执行任务,并返回一个ForkJoinTask
对象。由于ForkJoinTask
实现了Future
接口,你可以通过这个返回的对象来检查任务状态或获取结果(例如调用get()
方法)。
来看一个测试代码中的例子,它使用
invoke
来启动计算并等待结果:Integrate.java
// ... existing code ... static final class SQuad extends RecursiveAction { static double computeArea(ForkJoinPool pool, double l, double r) { SQuad q = new SQuad(l, r, 0); pool.invoke(q); return q.area; } // ... existing code ...
-
内部提交 (在
ForkJoinTask
的compute
方法内):fork()
: 这是核心的分解方法。它将任务异步地推入当前工作线程的队列头部,然后立即返回,当前线程可以继续执行其他操作。join()
: 阻塞等待一个已经被fork
的任务执行完成,并获取其结果。如果任务尚未完成,调用join()
的线程可能会去执行其他待处理的任务(包括帮助完成这个join的任务),以避免空闲等待,这是ForkJoinPool
效率的又一体现。
一个典型的分治模式如下:
if (问题足够小) { 直接计算结果; } else { // 分解 SubTask left = new SubTask(...); SubTask right = new SubTask(...); // 异步执行子任务 left.fork(); right.fork(); // 等待并合并结果 Result leftResult = left.join(); Result rightResult = right.join(); return merge(leftResult, rightResult); }
ForkJoinPool 实现原理剖析
ForkJoinPool
的实现非常复杂和精巧,它大量使用了 Unsafe
和 CAS (Compare-And-Swap) 操作来追求极致的性能。我们从源码注释和关键组件来理解它。
核心数据结构:WorkQueue
正如 ForkJoinPool.java
的实现概述(Implementation Overview
)中所述,WorkQueue
是整个框架的核心。
// ... existing code ...
* WorkQueues
* ==========
*
* Most operations occur within work-stealing queues (in nested
* class WorkQueue). These are special forms of Deques that
* support only three of the four possible end-operations -- push,
* pop, and poll (aka steal), under the further constraints that
* push and pop are called only from the owning thread (or, as
* extended here, under a lock), while poll may be called from
* other threads.
// ... existing code ...
- 结构: 每个
WorkQueue
内部有一个循环数组来存储ForkJoinTask
。它有两个关键的 volatile 指针:top
和base
。top
: 指向队列的顶部,只有所有者线程可以修改它。push
(入队)和pop
(出队)操作会移动top
指针。base
: 指向队列的底部,所有者线程和窃取者线程都可能修改它。poll
或steal
(窃取)操作会移动base
指针。
- 操作:
push(task)
: 任务所有者线程调用,将任务放入array[top]
,然后top
加一。pop()
: 任务所有者线程调用,从array[top-1]
取出任务,然后top
减一。这是 LIFO 行为。poll()
(或steal()
): 窃取线程调用,从array[base]
取出任务,然后base
加一。这是 FIFO 行为。
- 并发控制:
top
和base
的更新以及对数组槽位的读写都通过精心设计的内存屏障和 CAS 操作来保证线程安全和可见性,避免了使用重量级的锁。
状态管理:ctl
和 runState
ForkJoinPool
将其核心控制状态打包在几个 long
或 int
类型的字段中,通过位运算进行高效的原子更新。
-
ctl
字段: 这是一个 64 位的long
,包含了极其丰富的信息。// ... existing code ... * Field "ctl" contains 64 bits holding information needed to * atomically decide to add, enqueue (on an event queue), and * dequeue and release workers. To enable this packing, we * restrict maximum parallelism to (1<<15)-1 (which is far in * excess of normal operating range) to allow ids, counts, and * their negations (used for thresholding) to fit into 16bit * subfields. // ... existing code ...
它被分成了几个 16 位的域,分别记录:
AC
(active count): 活跃的工作线程数。TC
(total count): 总的工作线程数。SS
(stack top): 等待任务的线程栈顶。ID
(id): 下一个要创建的窃取队列的ID。 通过对ctl
进行 CAS 操作,可以原子地修改多个状态,例如,同时增加活跃线程数和总线程数。
-
runState
字段: 这个int
字段记录了线程池的生命周期状态(如RUNNING
,SHUTDOWN
,STOP
等),同时也包含了一个版本号,用于在更新queues
数组时充当一个轻量级锁(seqLock)。
线程管理
ForkJoinPool
动态地管理其工作线程,以维持期望的并行度。
- 补偿线程 (Compensation Threads): 当一个工作线程在执行
join()
操作时,如果它等待的任务还没有完成,它可能会被阻塞。为了不降低整体的并行度,ForkJoinPool
可能会创建一个“补偿线程”来顶替这个被阻塞的线程,继续执行其他任务。这是防止因任务依赖而导致线程池饥饿和死锁的重要机制。 - 线程的创建与销毁: 当有新任务提交且没有足够的活跃线程时,线程池会创建新线程。当线程长时间空闲时,它会自动终止并从池中移除。
任务提交与扫描
- 外部提交队列 (Submission Queues): 从外部(非
ForkJoinWorkerThread
)提交的任务不会直接进入某个工作线程的私有队列,而是被放入一个或多个共享的“提交队列”中。这些队列也位于queues
数组中,但处于偶数索引位,而工作线程的队列在奇数索引位。工作线程在自己队列为空时,也会去扫描这些提交队列。 - 任务扫描 (Scanning): 一个空闲的工作线程寻找任务的顺序大致是:
- 尝试从自己的队列头部
pop
一个任务 (LIFO)。 - 如果失败,则开始扫描,随机选择一个其他工作线程的队列,尝试从其尾部
poll
(steal) 一个任务 (FIFO)。 - 如果还失败,会尝试从外部提交队列中获取任务。
- 如果所有地方都找不到任务,线程最终会进入休眠(
park
),等待被唤醒。
- 尝试从自己的队列头部
总结
ForkJoinPool
是一个为并行计算量身定做的、高度优化的执行框架。
- 从用户角度看,它通过简单的
fork()
和join()
API,让开发者能方便地实现“分而治之”的并行算法,而无需关心底层复杂的线程调度和负载均衡。 - 从实现角度看,它的高性能源于 工作窃取 算法,该算法通过每个线程维护自己的双端队列,并结合 LIFO/FIFO 的策略,最大化了 CPU 缓存效率和核心利用率。其内部实现利用了 CAS 和位运算等底层技术,避免了传统锁带来的开销,是 Java 并发编程中的一个杰作。
核心算法与实现难点
1. 工作窃取算法
ForkJoinPool
的核心是工作窃取算法,主要在 runWorker()
方法中实现:
final void runWorker(WorkQueue w) {
// 初始化随机数生成器,用于确定窃取顺序
int r = w.stackPred;
for (;;) {
// 扫描其他队列寻找任务,优先处理自己队列中的任务
// 如果自己队列为空,则尝试从其他队列窃取任务
// 如果没有任务可以窃取,则进入休眠状态
}
}
难点在于:
- 无锁窃取:通过 CAS 操作从其他队列底部窃取任务,避免锁竞争
- 随机扫描:使用伪随机算法确定窃取顺序,避免线程集中在少数几个队列上竞争
- 工作平衡:保持工作负载均衡,避免线程空闲
2. 任务调度与同步
在 helpJoin()
和 helpComplete()
方法中,实现了高效的任务等待机制:
final int helpJoin(ForkJoinTask<?> task, WorkQueue w, boolean internal) {
// 尝试在自己队列中查找并执行任务
// 如果没找到,则尝试查找可能拥有该任务的其他队列
// 通过追踪窃取链找到相关任务并执行
}
难点在于:
- 窃取追踪:实现
source
字段记录窃取来源,形成窃取链 - 自适应等待:在等待任务完成时不仅仅阻塞,而是帮助执行其他任务
- 避免死锁:处理任务依赖关系,避免产生死锁
3. 状态管理与线程控制
在 tryCompensate()
和 deactivate()
方法中,实现了复杂的线程状态管理:
private int tryCompensate(long c) {
// 尝试激活休眠线程、减少活跃线程数或创建新线程
// 基于当前池状态和配置做出决策
}
难点在于:
- 原子状态更新:使用
ctl
字段原子地管理池状态 - 活跃度控制:在保持足够并行度的同时避免创建过多线程
- 动态调整:根据负载情况动态调整线程数量
实现类似的类的关键点
如果要实现类似的工作窃取线程池,需要考虑以下关键点:
1. 基本结构
-
工作队列设计:采用双端队列(Deque)结构,支持 LIFO 和 FIFO 操作
-
线程管理:创建专用的工作线程类,类似于
ForkJoinWorkerThread
-
任务抽象:设计类似
ForkJoinTask
的任务类,支持分解和递归执行
2. 核心机制
-
工作窃取:实现从其他线程队列尾部窃取任务的逻辑
-
自适应调度:当线程空闲时尝试窃取,无任务可窃取时进入休眠
-
负载均衡:随机选择窃取目标,避免竞争热点
3. 性能优化
-
伪共享防护:使用
@Contended
或填充避免缓存行伪共享 -
无锁算法:尽量使用 CAS 操作替代锁
-
局部性优化:优先执行本地任务,提高缓存命中率
4. 健壮性考虑
- 异常处理:任务执行异常不应影响线程池整体运行
- 优雅关闭:支持有序关闭和强制关闭
- 监控和调试:提供丰富的统计信息和状态查询接口
任务提交方法实现分析
ForkJoinPool 提供了三种主要的任务提交入口方法:invoke
、submit
和 execute
。让我们分析它们的具体实现方式。
所有提交方法都依赖于内部方法 poolSubmit
:
private <T> ForkJoinTask<T> poolSubmit(boolean signalIfEmpty, ForkJoinTask<T> task) {
Thread t; ForkJoinWorkerThread wt; WorkQueue q; boolean internal;
if (((t = JLA.currentCarrierThread()) instanceof ForkJoinWorkerThread) &&
(wt = (ForkJoinWorkerThread)t).pool == this) {
// 如果当前线程是本池的工作线程,使用该线程的工作队列
internal = true;
q = wt.workQueue;
}
else {
// 否则找到或创建一个外部提交队列
internal = false;
q = submissionQueue(ThreadLocalRandom.getProbe(), true);
}
// 将任务推入队列,根据signalIfEmpty决定是否需要唤醒工作线程
q.push(task, signalIfEmpty ? this : null, internal);
return task;
}
这个方法有两个关键步骤:
- 确定提交队列:区分内部工作线程和外部调用线程
- 向队列推送任务:并根据
signalIfEmpty
决定是否需要唤醒工作线程
public <T> T invoke(ForkJoinTask<T> task) {
poolSubmit(true, Objects.requireNonNull(task));
try {
return task.join();
} catch (RuntimeException | Error unchecked) {
throw unchecked;
} catch (Exception checked) {
throw new RuntimeException(checked);
}
}
invoke
方法的特点:
- 调用
poolSubmit
,并传入signalIfEmpty=true
确保工作线程被唤醒 - 调用
task.join()
等待任务执行完成并获取结果 - 阻塞当前线程直到任务完成
- 会将所有受检异常包装成
RuntimeException
抛出
public <T> ForkJoinTask<T> submit(ForkJoinTask<T> task) {
return poolSubmit(true, Objects.requireNonNull(task));
}
submit
方法的特点:
- 与
invoke
类似,调用poolSubmit
并传入signalIfEmpty=true
- 不等待任务完成,立即返回任务对象
- 返回的任务对象可以稍后调用
join()
或get()
获取结果
其他重载版本如submit(Runnable)
会先包装成ForkJoinTask
:
public <T> ForkJoinTask<T> submit(Callable<T> task) {
Objects.requireNonNull(task);
return poolSubmit(
true,
(Thread.currentThread() instanceof ForkJoinWorkerThread) ?
new ForkJoinTask.AdaptedCallable<T>(task) :
new ForkJoinTask.AdaptedInterruptibleCallable<T>(task));
}
public void execute(ForkJoinTask<?> task) {
poolSubmit(true, Objects.requireNonNull(task));
}
@Override
@SuppressWarnings("unchecked")
public void execute(Runnable task) {
poolSubmit(true, (Objects.requireNonNull(task) instanceof ForkJoinTask<?>)
? (ForkJoinTask<Void>) task // 避免重复包装
: new ForkJoinTask.RunnableExecuteAction(task));
}
execute
方法的特点:
- 与
submit
类似调用poolSubmit
,但没有返回值 - 对于
Runnable
参数,会包装为ForkJoinTask
类型 - 任务异步执行,不会阻塞调用线程
- 不提供获取结果的便捷方式
三种方法的区别总结
方法 | 签名 | 阻塞 | 返回值 | 使用场景 |
---|---|---|---|---|
invoke | <T> T invoke(ForkJoinTask<T>) | 是 | 直接返回任务结果 | 需要等待任务完成并立即获取结果 |
submit | <T> ForkJoinTask<T> submit(ForkJoinTask<T>) | 否 | 返回任务本身 | 异步执行,稍后可通过返回的任务获取结果 |
execute | void execute(ForkJoinTask<?>) 或 void execute(Runnable) | 否 | 无 | 不关心任务结果,纯异步执行 |
所有这些方法核心都调用poolSubmit
,区别在于是否等待结果以及如何处理返回值。任务被提交到池中后,ForkJoinPool的工作窃取机制会确保任务得到有效执行。
submissionQueue方法
此方法为非工作线程( 不是本pool的ForkJoinWorkerThread) 寻找或创建一个工作队列来提交任务,并确保该队列被锁定以便安全地添加任务。如果池已关闭或终止,则抛出RejectedExecutionException。
ForkJoinPool
内部的工作线程都有自己的私有双端队列。但是,当一个外部线程(例如,你应用的主线程)调用 pool.submit(task)
或 pool.execute(task)
时,这个任务需要一个地方存放,以便工作线程可以发现并执行它。如果所有外部线程都往同一个全局队列里放任务,那么这个队列就会成为性能瓶颈。
submissionQueue
方法通过一种 分片(Striping) 的思想来解决这个问题。它维护一个 WorkQueue
数组(queues
),并将不同的外部线程映射到不同的队列上,从而分散竞争,提高并发提交的吞吐量。这个方法不仅要找到或创建一个队列,还要 在返回前将其锁定,以确保后续的任务入队操作是线程安全的。
让我们逐行分析代码的逻辑:
// ... existing code ...
private WorkQueue submissionQueue(int r, boolean rejectOnShutdown) {
int reuse; // nonzero if prefer create
if ((reuse = r) == 0) {
ThreadLocalRandom.localInit(); // initialize caller's probe
r = ThreadLocalRandom.getProbe();
}
for (int probes = 0; ; ++probes) {
int n, i, id; WorkQueue[] qs; WorkQueue q;
if ((qs = queues) == null)
break;
if ((n = qs.length) <= 0)
break;
if ((q = qs[i = (id = r & EXTERNAL_ID_MASK) & (n - 1)]) == null) {
WorkQueue w = new WorkQueue(null, id, 0, false);
w.phase = id;
boolean reject = ((lockRunState() & SHUTDOWN) != 0 &&
rejectOnShutdown);
if (!reject && queues == qs && qs[i] == null)
q = qs[i] = w; // else lost race to install
unlockRunState();
if (q != null)
return q;
if (reject)
break;
reuse = 0;
}
else if (reuse == 0 || !q.tryLockPhase()) { // move index
if (reuse == 0) {
if (probes >= n >> 1)
reuse = r; // stop prefering free slot
}
else if (q != null)
reuse = 0; // probe on collision
r = ThreadLocalRandom.advanceProbe(r);
}
else if (rejectOnShutdown && (runState & SHUTDOWN) != 0L) {
q.unlockPhase(); // check while q lock held
break;
}
else
return q;
}
throw new RejectedExecutionException();
}
// ... existing code ...
初始化线程探针 (Probe)
int reuse;
if ((reuse = r) == 0) {
ThreadLocalRandom.localInit();
r = ThreadLocalRandom.getProbe();
}
r
: 这个参数是ThreadLocalRandom.getProbe()
的值,可以看作是当前线程的一个“哈希码”或“探针值”。它被用来将线程映射到queues
数组的特定索引上。if (r == 0)
: 如果一个线程从未用过ThreadLocalRandom
,它的探针值就是 0。这里会为它初始化一个非零的探针值。这确保了每个外部线程都有一个用于分片的标识。reuse
: 这个变量用于控制后续的逻辑,初始值被设为探针r
。
方法的主体是一个无限循环 for (int probes = 0; ; ++probes)
,它会一直尝试,直到成功找到并锁定一个队列,或者确定该拒绝任务并抛出异常。
计算目标队列索引
i = (id = r & EXTERNAL_ID_MASK) & (n - 1)
这是分片逻辑的核心。
id = r & EXTERNAL_ID_MASK
:EXTERNAL_ID_MASK
是一个掩码,它确保计算出的id
具有特定标志位,用以区分这是外部提交队列还是内部工作队列。& (n - 1)
:queues
数组的长度n
总是2的幂。因此,& (n - 1)
是一个非常高效的取模运算 (% n
),它将线程的id
映射到数组的一个有效索引i
上。
场景一:目标槽位为空 (q == null
)
如果计算出的索引 i
对应的队列为空,代码会尝试创建一个新的队列并放入该槽位。
if ((q = qs[i = ...]) == null) {
WorkQueue w = new WorkQueue(null, id, 0, false); // 1. 创建新队列
w.phase = id;
boolean reject = ((lockRunState() & SHUTDOWN) != 0 && ...); // 2. 锁定并检查状态
if (!reject && queues == qs && qs[i] == null)
q = qs[i] = w; // 3. 安装队列 (CAS-like)
unlockRunState();
if (q != null)
return q; // 4. 成功,返回
if (reject)
break; // 5. 如果需拒绝,跳出循环
reuse = 0; // 6. 安装失败,下次循环强制探测
}
这是一个经典的 双重检查锁定 (Double-Checked Locking) 模式的变体。
- 创建实例:
new WorkQueue(...)
创建一个新的外部队列实例。 - 加锁与检查:
lockRunState()
获取一个轻量级锁,然后检查线程池是否已关闭。 - 安装队列: 在持有锁的情况下,再次检查
qs[i] == null
。这是为了防止在当前线程检查到null
并准备创建队列的间隙,另一个线程已经成功地在这个位置安装了队列。如果槽位仍然是null
,就安全地将新队列放入。 - 成功返回: 如果
q
不为null
(表示安装成功),直接返回这个新创建的队列。 - 失败处理: 如果因为线程池关闭而需要拒绝任务,则跳出循环,最后抛出异常。
- 竞争失败: 如果安装失败(即
qs[i]
在加锁后发现不是null
了),说明输掉了竞争。将reuse
设为 0,这样在下一次循环中就会进入下面的冲突处理逻辑。
场景二:目标槽位非空 (发生冲突)
如果目标槽位已经有队列了,或者创建新队列时输掉了竞争,就会进入冲突处理逻辑。
else if (reuse == 0 || !q.tryLockPhase()) {
// ...
r = ThreadLocalRandom.advanceProbe(r);
}
!q.tryLockPhase()
:tryLockPhase
是一个非阻塞的 CAS 操作,尝试锁定这个已存在的队列q
。如果返回false
,说明这个队列正被其他线程使用,发生了 真冲突。reuse == 0
: 如果reuse
为 0(说明在上一步创建队列时竞争失败,或者之前已经发生过冲突),也会进入这个分支。r = ThreadLocalRandom.advanceProbe(r)
: 这是解决冲突的关键。它会根据当前探针r
计算出一个新的探针值。在下一次循环中,这个新的r
值会被映射到一个不同的数组索引,从而实现了 线性探测,避免了在同一个槽位上死等。
场景三:成功锁定已有队列
else if (rejectOnShutdown && (runState & SHUTDOWN) != 0L) {
q.unlockPhase();
break;
}
else
return q;
- 如果
tryLockPhase()
返回true
,说明成功锁定了队列q
。 - 在返回之前,必须 再次检查 线程池的关闭状态。这是因为从进入方法到成功锁定队列之间,线程池的状态可能已经改变。
- 如果线程池已关闭,就释放锁并跳出循环,准备抛出异常。
- 如果一切正常,就返回这个已成功锁定的队列
q
。
总结
submissionQueue
方法是一个设计精巧、性能极高的组件。它通过以下机制实现了对外部任务提交的高效分发和管理:
- 线程探针 (
Probe
):利用ThreadLocalRandom
为每个外部线程生成一个探针值。 - 分片/分桶 (
Striping
):通过探针值将线程映射到queues
数组的不同槽位,分散了提交任务时的竞争。 - 无锁化尝试 (
tryLockPhase
):使用 CAS 操作对队列进行非阻塞加锁,避免了重量级锁的开销。 - 线性探测 (
advanceProbe
):当发生冲突时,通过改变探针值来探测下一个槽位,而不是在原地自旋等待。 - 双重检查锁定:在创建新队列时,安全高效地处理并发创建的竞争条件。
WorkQueue 与 线程的关系
在 ForkJoinPool 中,存在两种类型的队列:内部队列(worker queues) 和 外部队列(submission queues)。
外部队列主要作为任务提交的"缓存区",而内部队列上的工作线程负责从这些外部队列中获取和执行任务。
内部队列 (Worker Queues)
- 索引特点:奇数索引 (odd indices)
- 拥有者:有具体的 ForkJoinWorkerThread 作为 owner
- 用途:由工作线程使用,存储它们正在处理的任务及其派生的子任务
- 访问方式:无需加锁即可访问 (由拥有线程直接操作)
外部队列 (Submission Queues)
- 索引特点:偶数索引 (even indices)
- 拥有者:owner 为 null
- 用途:接收从外部提交的任务 (通过 submit、execute 等方法)
- 访问方式:需要通过锁 (phase 锁) 来同步访问
在代码中可以看到这种区分:
// 创建和注册工作队列
final void registerWorker(WorkQueue w) {
// ...
// 工作线程队列的索引总是奇数
int id = ((seed << 1) | 1) & SMASK;
// ...
}
// 查看外部提交队列
public boolean hasQueuedSubmissions() {
// ...
// 遍历偶数索引的队列 (i += 2)
for (int i = 0; i < qs.length; i += 2) {
if ((q = qs[i]) != null && q.queueSize() > 0)
return true;
}
return false;
}
// 在quiescent()方法中
for (int i = 0; i < n; ++i) {
// 可以看到遍历所有队列
}
// 在getQueuedSubmissionCount()方法中
for (int i = 0; i < qs.length; i += 2) {
// 注意这里i += 2,只遍历偶数索引位置,即外部队列
}
内部/外部之分主要体现在:
-
队列类型:
- 奇数索引的工作线程队列 (内部)
- 偶数索引的提交队列 (外部)
-
提交来源:
- 任务由 ForkJoinWorkerThread 提交 (内部)
- 任务由外部线程提交 (外部)
-
访问模式:
- 内部队列由其所有者线程直接访问,无需加锁
- 外部队列需要加锁访问
这种区分使 ForkJoinPool 能高效地处理不同来源的任务,同时保持工作窃取算法的高性能。
if (((t = JLA.currentCarrierThread()) instanceof ForkJoinWorkerThread) &&
(wt = (ForkJoinWorkerThread)t).pool == this) {
internal = true;
q = wt.workQueue;
}
这段代码的目的是判断当前调用线程是否是本池中的工作线程,有以下关键点:
-
JLA.currentCarrierThread():
- 这是JavaLangAccess的方法,获取当前执行线程
- 比Thread.currentThread()更通用,能处理虚拟线程
- 主要用于区分是内部工作线程还是外部提交线程
-
判断条件:
- 第一个条件:线程是ForkJoinWorkerThread类型
- 第二个条件:线程所属的池就是当前池(this)
-
结果处理:
- 如果条件满足:设置internal=true,使用线程自己的工作队列
- 否则(条件不满足):设置internal=false,需要找一个外部提交队列
signalWork 方法
signalWork
是 ForkJoinPool 中非常核心的方法,负责唤醒或创建工作线程来处理任务。当任务被提交到池中,或者工作窃取过程中发现需要更多工作线程时,就会调用这个方法:
- 功能: 确保有足够的工作线程来处理提交的任务
- 策略: 优先唤醒空闲线程,必要时创建新线程
- 实现: 使用无锁算法和 Treiber 栈进行高效管理
- 触发点:
- 任务提交到空队列时
- 队列需要扩容时
- 窃取任务时发现更多任务需要处理
final void signalWork() {
int pc = parallelism;
for (long c = ctl;;) {
WorkQueue[] qs = queues;
long ac = (c + RC_UNIT) & RC_MASK, nc;
int sp = (int)c, i = sp & SMASK;
if ((short)(c >>> RC_SHIFT) >= pc)
break; // 1. 活跃线程已足够
if (qs == null)
break; // 2. 队列数组未初始化
if (qs.length <= i)
break; // 3. 索引超出范围
WorkQueue w = qs[i], v = null;
if (sp == 0) { // 4a. 没有空闲工作线程
if ((short)(c >>> TC_SHIFT) >= pc)
break; // 5. 总线程数已达上限
nc = ((c + TC_UNIT) & TC_MASK); // 增加总线程计数
}
else if ((v = w) == null) // 4b. 空闲队列槽位为空
break;
else
nc = (v.stackPred & LMASK) | (c & TC_MASK); // 取下一个空闲工作线程
if (c == (c = compareAndExchangeCtl(c, nc | ac))) {
if (v == null) // 6. 创建新工作线程
createWorker();
else { // 7. 唤醒空闲工作线程
v.phase = sp;
if (v.parking != 0)
U.unpark(v.owner);
}
break;
}
}
}
详细执行流程分析
1. 状态检查与准备
int pc = parallelism;
for (long c = ctl;;) {
// ...
if ((short)(c >>> RC_SHIFT) >= pc)
break;
pc
获取池的并行度(目标工作线程数量)ctl
是一个 64 位的状态字段,编码了:- 高 16 位 (RC): 当前活跃(已释放)的工作线程数
- 中 16 位 (TC): 池中总线程数
- 低 32 位: 空闲线程栈的顶部信息
- 如果活跃线程数已经达到或超过并行度,则不需要唤醒或创建新线程
2. 确定唤醒策略
在检查完基本条件后,方法决定是创建新线程还是唤醒现有空闲线程:
if (sp == 0) { // 没有空闲工作线程
if ((short)(c >>> TC_SHIFT) >= pc)
break; // 总线程数已达上限
nc = ((c + TC_UNIT) & TC_MASK); // 增加总线程计数
}
else if ((v = w) == null) // 空闲队列槽位为空
break;
else
nc = (v.stackPred & LMASK) | (c & TC_MASK); // 取下一个空闲工作线程
这段代码分为两种情况:
sp == 0
: 空闲线程栈为空(没有空闲工作线程)- 需要创建新线程,但先检查总线程数是否已达上限
- 如未达上限,准备增加总线程计数
sp != 0
: 有空闲工作线程- 获取栈顶空闲工作队列
- 如果队列为空则退出
- 否则,准备弹出栈顶元素(下一个
ctl
会指向当前栈顶的前驱)
3. CAS 更新状态并执行操作
if (c == (c = compareAndExchangeCtl(c, nc | ac))) {
if (v == null) // 创建新工作线程
createWorker();
else { // 唤醒空闲工作线程
v.phase = sp;
if (v.parking != 0)
U.unpark(v.owner);
}
break;
}
- 使用 CAS 操作尝试更新
ctl
- 成功后,根据之前的决策:
- 如果
v == null
(创建新线程),调用createWorker()
- 否则,唤醒空闲线程:
- 设置工作队列的
phase
为当前sp
值(表示激活) - 如果线程已经 parking,则调用
Unsafe.unpark
- 设置工作队列的
- 如果
代码解析:如何拿到并唤醒线程
WorkQueue w = qs[i], v = null;
if (sp == 0) {
// 没有空闲线程的情况,创建新线程
// ...
}
else if ((v = w) == null)
break;
else
nc = (v.stackPred & LMASK) | (c & TC_MASK);
这里的关键点:
- 从
ctl
提取出sp
(stack pointer,通过int强转 获得 ctl的低32位),这是一个指向待唤醒的工作队列的索引 - 如果
sp != 0
,说明有空闲线程,将w
赋值给v
- 此时
v
是从空闲队列栈中获取的WorkQueue
对象
当 CAS 操作成功时,通过 v.owner
获取线程并唤醒它:
v
是工作队列对象v.owner
是这个工作队列对应的ForkJoinWorkerThread
线程对象U.unpark(v.owner)
唤醒这个线程
这个函数不会尝试在外部提交队列上调用 U.unpark(v.owner),因为这些队列不会被选为唤醒目标。
ForkJoinPool 使用 Treiber 栈(一种无锁栈结构)来管理空闲线程:
- 线程空闲时,在
deactivate()
方法中将自己的 WorkQueue 放入空闲栈 - 需要工作线程时,从栈顶取出一个 WorkQueue,通过其 owner 引用唤醒对应线程
为什么能通过 v 拿到线程?因为:
v
是WorkQueue
类型的对象- 每个
WorkQueue
对象都有一个owner
字段,指向对应的工作线程 - 当一个线程成为空闲线程时,会保留其 WorkQueue 对象在栈中
- 唤醒时,从栈中取出 WorkQueue,然后通过
v.owner
访问关联的线程
链式调用流程分析
当 signalWork()
被调用时,它会启动一系列操作。让我们分析整个调用链:
调用入口: q.push 引起 signalWork 调用
final void push(ForkJoinTask<?> task, ForkJoinPool pool, boolean internal) {
// ... 省略前面的代码
if ((room == 0 || a[m & (s - pk)] == null) && pool != null)
pool.signalWork(); // may have appeared empty
// ...
}
当以下任意条件满足时,会调用 signalWork()
:
- 队列已满需要扩容 (
room == 0
) - 队列之前为空但现在有了任务 (
a[m & (s - pk)] == null
)
路径 1: 创建新线程
signalWork()
-> compareAndExchangeCtl() // CAS 更新 ctl
-> createWorker() // 创建新工作线程
-> factory.newThread(this) // 通过工厂创建 ForkJoinWorkerThread
-> ctr.start(wt) 或 wt.start() // 启动线程
线程启动后会调用:
ForkJoinWorkerThread.run()
-> pool.registerWorker(workQueue) // 注册工作队列
-> pool.runWorker(workQueue) // 运行工作循环
路径 2: 唤醒已有线程
signalWork()
-> compareAndExchangeCtl() // CAS 更新 ctl
-> v.phase = sp // 激活线程
-> U.unpark(v.owner) // 唤醒线程
线程被唤醒后恢复执行:
被唤醒的线程从 awaitWork() 或 deactivate() 方法中返回
-> 继续执行 runWorker() 工作循环
关键细节解释
1. 工作线程状态管理
signalWork
与 deactivate
是互补的:
deactivate
: 当无任务可执行时,工作线程自我挂起并加入空闲线程栈signalWork
: 当有任务需要执行时,从栈中弹出线程并唤醒,或创建新线程
2. ctl 字段的位操作
ctl
字段包含多个计数器和状态信息,通过位操作高效处理:
63 48 47 32 31 0
+----------+----------+-----------------------+
| RC | TC | stack top |
+----------+----------+-----------------------+
- RC (Release Count): 活跃工作线程计数
- TC (Total Count): 总线程计数
- 低 32 位: 空闲线程栈顶的 WorkQueue 信息
3. Treiber 栈实现
空闲工作线程通过 Treiber 栈管理(一种无锁栈):
stackPred
字段存储前一个空闲线程的信息phase
字段保存线程标识和状态
4. 工作窃取触发的信号传播
在 WorkQueue.runWorker
方法中:
if (propagate = (prevSrc != src || nh != 0) && a[nb & m] != null)
signalWork();
当一个工作线程窃取任务并发现以下情况时,也会调用 signalWork()
:
- 窃取源发生变化 (
prevSrc != src
) - 任务不允许用户线程帮助执行 (
nh != 0
) - 被窃取队列中仍有更多任务 (
a[nb & m] != null
)
工作线程的注册与反注册:线程管理机制分析
ForkJoinPool 中的 registerWorker
和 deregisterWorker
方法是线程池管理工作线程的关键机制,它们处理工作线程的生命周期管理。下面详细分析这两个方法的工作原理和流程。
总体设计要点
-
索引分配策略:
- 工作线程队列总是位于奇数索引
- 外部提交队列位于偶数索引
- 使用散列和线性探测查找可用位置
-
线程计数管理:
ctl
字段包含多个计数器(活跃线程数、总线程数)- 使用 CAS 操作保证计数更新的原子性
-
工作队列状态维护:
- 通过
phase
字段跟踪队列的状态和索引 - 通过
stackPred
维护工作线程的栈结构
- 通过
-
资源回收与平衡:
- 工作线程终止时累积窃取统计
- 在必要时创建新线程保持并行度
- 清理终止线程队列中的任务
这两个方法共同构成了 ForkJoinPool 动态管理工作线程池大小的核心机制,使线程池能够根据负载情况自适应地调整工作线程数量。
registerWorker (手动内存屏障)
registerWorker
方法负责将新创建的工作线程及其关联的 WorkQueue 注册到 ForkJoinPool 中:
1. 初始化与准备工作
ThreadLocalRandom.localInit();
int seed = w.stackPred = ThreadLocalRandom.getProbe();
-
初始化工作线程的
ThreadLocalRandom
-
使用随机种子设置
stackPred
,后续用于工作窃取和任务调度
2. 计算工作队列标识和位置
int phaseSeq = seed & ~((IDLE << 1) - 1); // 初始 phase 标记
int id = ((seed << 1) | 1) & SMASK; // 工作队列索引基础
phaseSeq
:生成不包含 IDLE 位的初始 phase 标记id
:生成奇数索引(通过| 1
),确保工作线程队列总是放在奇数位置
3. 队列数组中分配位置
for (int k = n, m = n - 1; ; id += 2) {
if (qs[id &= m] == null)
break;
if ((k -= 2) <= 0) {
id |= n;
break;
}
}
-
采用线性探测法查找可用槽位(始终保持奇数索引,所以每次 +2)
-
如果现有数组装不下(循环 n/2 次仍未找到空位),则设置扩容标记
id |= n
4. 队列注册与数组扩容
w.phase = id | phaseSeq; // 设置队列的标识符
if (id < n)
qs[id] = w; // 直接注册到现有数组
else { // 需要扩容
int an = n << 1, am = an - 1; // 扩大为两倍
WorkQueue[] as = new WorkQueue[an];
// 注册新的工作队列
as[id & am] = w;
// 迁移现有工作队列(奇数位置)
for (int j = 1; j < n; j += 2)
as[j] = qs[j];
// 迁移共享队列(偶数位置)
for (int j = 0; j < n; j += 2) {
WorkQueue q;
if ((q = qs[j]) != null)
as[q.phase & EXTERNAL_ID_MASK & am] = q;
}
U.storeFence(); // 内存屏障确保所有写入对其他线程可见
queues = as; // 替换队列数组
}
- 对新的工作队列设置
phase
标识符(包含索引和状态信息) - 处理两种情况:直接注册或扩容后注册
- 扩容时区别对待工作队列(奇数位置)和共享队列(偶数位置)
- 使用内存屏障确保可见性
- 如果没有内存屏障,JVM或CPU可能会重排指令,导致
queues = as
在数组填充完成前执行。这会使其他线程看到一个尚未完全初始化的队列数组。
deregisterWorker
deregisterWorker
方法在工作线程终止时被调用,用于清理资源并维护线程池状态:
1. 释放等待的工作线程
if (wt != null && (w = wt.workQueue) != null &&
(phase = w.phase) != 0 && (phase & IDLE) != 0)
releaseWaiters(); // 确保释放所有等待线程
-
若工作线程处于空闲状态,调用
releaseWaiters()
唤醒其它可能在等待的线程
2. 减少线程计数
if (w == null || w.source != DROPPED) {
long c = ctl;
do {} while (c != (c = compareAndExchangeCtl(
c, ((RC_MASK & (c - RC_UNIT)) |
(TC_MASK & (c - TC_UNIT)) |
(LMASK & c)))));
}
- 减少活跃线程计数(RC_UNIT)和总线程计数(TC_UNIT)
- 使用 CAS 操作确保原子性更新
- 如果队列已被标记为
DROPPED
,则跳过计数减少(避免重复操作)
3. 从队列数组中移除工作队列
if (phase != 0 && w != null) {
long ns = w.nsteals & 0xffffffffL;
if ((runState & STOP) == 0L) {
if ((lockRunState() & STOP) == 0L &&
(qs = queues) != null && (n = qs.length) > 0 &&
qs[i = phase & SMASK & (n - 1)] == w) {
qs[i] = null;
stealCount += ns; // 累积窃取统计
}
unlockRunState();
}
}
- 计算工作队列在数组中的位置,并将其移除
- 将工作线程的窃取计数累加到池的总窃取计数
- 使用
lockRunState
/unlockRunState
同步访问队列数组
4. 资源清理与池状态维护
if ((tryTerminate(false, false) & STOP) == 0L &&
phase != 0 && w != null && w.source != DROPPED) {
signalWork(); // 可能需要替换工作线程
w.cancelTasks(); // 清理队列中的剩余任务
}
-
检查是否需要终止线程池
-
如果池未终止,则尝试通过
signalWork()
创建新线程替代当前线程 -
取消队列中剩余的任务
5. 异常处理
if (ex != null)
ForkJoinTask.rethrow(ex);
- 如果线程终止是由异常引起的,重新抛出该异常
runWorker方法
runWorker
方法是ForkJoinPool的核心工作循环,由ForkJoinWorkerThread的run方法调用。这个方法实现了工作窃取(work-stealing)算法的核心逻辑,让工作线程不断寻找并执行任务。
/**
* Top-level runloop for workers, called by ForkJoinWorkerThread.run.
* See above for explanation.
*
* @param w caller's WorkQueue (may be null on failed initialization)
*/
final void runWorker(WorkQueue w) {
if (w != null) {
int phase = w.phase, r = w.stackPred; // seed from registerWorker
int fifo = w.config & FIFO, nsteals = 0, src = -1;
for (;;) {
WorkQueue[] qs;
r ^= r << 13; r ^= r >>> 17; r ^= r << 5; // xorshift
if ((runState & STOP) != 0L || (qs = queues) == null)
break;
int n = qs.length, i = r, step = (r >>> 16) | 1;
boolean rescan = false;
scan: for (int l = n; l > 0; --l, i += step) { // scan queues
int j, cap; WorkQueue q; ForkJoinTask<?>[] a;
if ((q = qs[j = i & (n - 1)]) != null &&
(a = q.array) != null && (cap = a.length) > 0) {
for (int m = cap - 1, pb = -1, b = q.base;;) {
ForkJoinTask<?> t; long k;
t = (ForkJoinTask<?>)U.getReferenceAcquire(
a, k = slotOffset(m & b));
if (b != (b = q.base) || t == null ||
!U.compareAndSetReference(a, k, t, null)) {
if (a[b & m] == null) {
if (rescan) // end of run
break scan;
if (a[(b + 1) & m] == null &&
a[(b + 2) & m] == null) {
break; // probably empty
}
if (pb == (pb = b)) { // track progress
rescan = true; // stalled; reorder scan
break scan;
}
}
}
else {
boolean propagate;
int nb = q.base = b + 1, prevSrc = src;
w.nsteals = ++nsteals;
w.source = src = j; // volatile
rescan = true;
int nh = t.noUserHelp();
if (propagate =
(prevSrc != src || nh != 0) && a[nb & m] != null)
signalWork();
w.topLevelExec(t, fifo);
if ((b = q.base) != nb && !propagate)
break scan; // reduce interference
}
}
}
}
if (!rescan) {
if (((phase = deactivate(w, phase)) & IDLE) != 0)
break;
src = -1; // re-enable propagation
}
}
}
}
在进入主循环之前,方法会初始化一些关键的局部变量:
- w: 当前工作线程自己的 WorkQueue。
- phase: 工作线程的状态,如 IDLE(空闲)。
- r: 一个随机数种子,用于后续的随机扫描。它从 w.stackPred 初始化,这个值在工作线程注册时 (registerWorker) 就已经设置好了。
- fifo: 一个配置,决定了当前工作线程在执行自己队列中的任务时,是采用 FIFO(先进先出)还是 LIFO(后进先出,默认)模式。
- nsteals: 记录当前线程成功窃取了多少个任务。
- src: 记录上一个窃取任务的来源队列索引。
if (w != null) {
int phase = w.phase, r = w.stackPred; // seed from registerWorker
int fifo = w.config & FIFO, nsteals = 0, src = -1;
for (;;) {
// 主循环内容...
}
}
随机数生成与扫描准备
r ^= r << 13; r ^= r >>> 17; r ^= r << 5; // xorshift
if ((runState & STOP) != 0L || (qs = queues) == null)
break;
int n = qs.length, i = r, step = (r >>> 16) | 1;
boolean rescan = false;
- xorshift算法: 生成随机数,用于随机选择要扫描的队列
- 终止检查: 如果池已停止或队列数组为空,则退出
- step计算: 生成一个奇数步长,确保扫描能覆盖所有队列
- rescan标志: 控制是否需要重新扫描队列
主体是无限循环,代表了工作线程的生命周期。线程会不断地在这个循环里寻找任务、执行任务,直到线程池关闭或自己被终止。
scan: for (int l = n; l > 0; --l, i += step) { // scan queues
int j, cap; WorkQueue q; ForkJoinTask<?>[] a;
if ((q = qs[j = i & (n - 1)]) != null &&
(a = q.array) != null && (cap = a.length) > 0) {
// 队列任务处理逻辑...
}
}
- 队列循环: 使用
scan:
标签定义的循环,扫描所有队列 - 索引计算:
j = i & (n - 1)
计算实际队列索引(环形访问) - 队列检查: 确保队列及其任务数组存在且有容量
工作窃取扫描 (scan: for (...)
)
当一个工作线程自己的任务队列为空时(注意:runWorker
的逻辑是先窃取,如果窃取不到再看自己的队列,这和我们通常理解的“先做自己的再偷”在代码实现上有所不同,但效果等价),它就会开始这个扫描循环,尝试从其他队列中“偷”一个任务。
-
随机化扫描:
r ^= r << 13; r ^= r >>> 17; r ^= r << 5;
: 这是一个xorshift
算法,用于高效地生成伪随机数。i = r
,step = (r >>> 16) | 1
: 扫描不是从索引0开始,而是从一个随机位置i
开始,并以一个奇数步长step
进行探测。这可以有效避免多个空闲线程同时扫描相同的队列,减少了不必要的竞争,并保证了扫描的公平性。
-
窃取任务:
q = qs[j = i & (n - 1)]
: 通过位运算高效地从queues
数组中取出一个目标队列q
。t = (ForkJoinTask<?>)U.getReferenceAcquire(...)
: 使用Unsafe
以Acquire
内存屏障模式,原子性地读取目标队列q
的底部 (base
指针位置) 的任务。if (b != (b = q.base) || t == null || !U.compareAndSetReference(a, k, t, null))
: 这是一个非常精巧的无锁检查。它判断窃取是否失败,有三种情况:b != q.base
: 在读取任务的瞬间,队列的base
指针被其他线程(队列所有者或其他窃取者)修改了,说明发生了竞争,需要重试。t == null
: 那个位置本来就是空的。!U.compareAndSetReference(...)
: CAS 操作失败。这表示在你读取到任务t
和你尝试将该位置设为null
之间,已经有另一个线程捷足先登把任务偷走了。
- 窃取成功 (
else
分支):q.base = b + 1
: 将目标队列的base
指针加一,正式完成窃取。w.nsteals = ++nsteals; w.source = src = j;
: 更新自己的窃取统计,并记录下是从哪个队列偷来的。signalWork()
: 这是一个重要的优化。如果发现被窃取的队列里还有任务,就可能会调用signalWork()
来唤醒另一个空闲的线程,或者创建一个新线程。因为一个任务的发现可能意味着将有一批新任务产生(通过fork
),提前唤醒帮手可以提高并行度。w.topLevelExec(t, fifo);
: 执行任务! 这是最关键的一步。当前工作线程开始执行偷来的任务t
。这是一个阻塞调用,直到这个任务及其派生的所有子任务都完成。rescan = true
: 标记本次大循环找到了任务。
找不到任务与休眠 (if (!rescan)
)
如果内层的 scan
循环跑了一整圈都没有找到任何任务(rescan
仍为 false
),说明整个线程池可能都处于空闲状态。
phase = deactivate(w, phase)
: 线程会调用deactivate
方法尝试将自己变为非活跃状态。它会把自己放到一个由ctl
变量维护的空闲线程栈中,并准备休眠。if (((phase = ...) & IDLE) != 0) break;
: 如果deactivate
的返回值表明线程应该终止(比如线程池关闭了,或者线程空闲超时了),则跳出主循环,结束线程生命周期。否则,线程会通过LockSupport.park
等待,直到被signalWork()
唤醒。
工作线程(Worker)什么时候处理自己的队列?
单看 runWorker
的主循环,确实会发现它一直在 scan
(扫描)并尝试从别的队列窃取任务。这似乎与我们通常理解的“先做完自己的事,再帮别人”的直觉相悖。
答案藏在任务的执行流程中。runWorker
的扫描循环,是工作线程在 彻底空闲 状态下寻找 任何一个“入口”任务 的行为。一旦它通过窃取(steal
)成功获得了一个任务 t
,它就会调用 w.topLevelExec(t, fifo)
来执行这个任务。
真正的魔法发生在 topLevelExec
以及任务的 join
过程中:
- 偷来的任务是“种子”: 工作线程A偷来了任务T1。现在,线程A开始执行T1。
- 任务分解产生本地任务: 在T1的
compute()
方法中,它很可能会被分解(fork()
)成两个子任务,比如T1a和T1b。这两个新产生的子任务T1a和T1b,会被push
到线程A 自己的工作队列 的队头(LIFO)。 join
优先处理本地任务: 接下来,T1的代码会调用T1a.join()
和T1b.join()
来等待子任务完成。在等待期间,线程A并不会闲着,join
的内部逻辑会驱使它去执行其他任务以避免阻塞。此时,它会优先检查并执行自己队列中的任务。因为它刚刚才把T1a和T1b放进去,所以它会立刻从自己的队列头把T1b(后进)拿出来执行,执行完再拿出T1a执行。
runLevelExec会调用task.doExec,调用用户自定义代码,可能使用fork,而fork会判断是不是公共线程,加入相应队列。
public final ForkJoinTask<V> fork() {
Thread t; ForkJoinWorkerThread wt;
ForkJoinPool p; ForkJoinPool.WorkQueue q; boolean internal;
if (internal =
(t = Thread.currentThread()) instanceof ForkJoinWorkerThread) {
q = (wt = (ForkJoinWorkerThread)t).workQueue;
p = wt.pool;
}
else
q = (p = ForkJoinPool.common).externalSubmissionQueue();
q.push(this, p, internal);
return this;
}
总结一下:
runWorker
的外层窃取循环,是线程在 “饥饿” 状态下找饭吃的行为。- 一旦偷到一个任务并开始执行,这个任务派生出的所有子任务都会进入它 自己的队列。
- 在等待子任务
join
的过程中,线程会 优先、且深度地处理自己队列中的任务(LIFO)。这利用了CPU缓存的局部性原理,因为刚创建的任务数据最“热”。 - 只有当自己的活儿干完了(在
join
等待期间发现自己队列也空了),它才会再次尝试去窃取,以帮助完成它正在等待的那个join
的任务。
所以,处理自己队列的逻辑,是嵌入在任务执行与join
的流程中的,而不是在runWorker
的顶层循环里直接体现。
为什么要 signalWork
?自己拿了任务是不是可能拿完了?
它揭示了 ForkJoinPool
如何动态地自我调节以达到最大吞吐量。
a. 自己拿了任务是不是可能拿完了?
是的,完全可能。所以代码在调用 signalWork
之前做了精确的检查。
我们来看 runWorker
中窃取成功后的关键判断:
// ... existing code ...
else {
boolean propagate;
int nb = q.base = b + 1, prevSrc = src;
w.nsteals = ++nsteals;
w.source = src = j; // volatile
rescan = true;
int nh = t.noUserHelp();
if (propagate =
(prevSrc != src || nh != 0) && a[nb & m] != null)
signalWork();
w.topLevelExec(t, fifo);
// ... existing code ...
这里的 if
条件 a[nb & m] != null
是核心。
b
是窃取前队列的base
(队尾)指针。nb = q.base = b + 1
是窃取后更新的队尾指针。a[nb & m]
就是在检查被窃取队列的 下一个位置 是否还有任务。
所以,signalWork()
只有在确认偷完一个之后,那个队列里至少还剩一个任务时,才有可能被触发。它不会在把别人队列偷空后还去“摇人”。
b. 为什么要 signalWork
?
调用 signalWork
的目的是 “主动的、前瞻性的负载均衡”。
可以把它想象成一个场景:一个工人在工地上发现了一大堆刚卸下的砖头(一个有多个任务的队列)。他自己开始搬(topLevelExec
),但同时他没有忘记朝远处还在休息的工友们大喊一声:“嘿!这里有活干了,快来帮忙!”(signalWork
)。
这么做有几个巨大的好处:
- 快速激活整个线程池:一个任务的提交,特别是那种可以被高度分解的大任务,往往意味着接下来会产生海量的子任务。如果只靠偷到它的那个线程自己干,然后再等其他空闲线程慢慢通过随机扫描发现这个“热点”,效率会很低。通过
signalWork
,可以形成一个 “信号链” 或 “级联唤醒”,迅速将整个线程池的算力都调动起来,共同处理这个任务簇。 - 避免线程饥饿:防止出现“旱的旱死,涝的涝死”的情况。一个线程忙于分解任务,把自己的队列塞满,而其他线程却因为随机扫描没扫描到它而处于空闲状态。
- 创建补偿线程:
signalWork
不仅会唤醒已有的空闲线程,如果发现活跃线程数低于并行度(parallelism
),它甚至可能会去调用createWorker()
来 创建一个新的工作线程,以保证计算资源被充分利用。
总而言之,signalWork
是 ForkJoinPool
实现高性能的关键机制之一。它体现了“信息共享”的思想,一个线程的发现可以迅速转化为整个集体的行动,从而实现对突发性、高并发任务的快速响应。
关键子函数解析
1. deactivate(WorkQueue w, int phase)
这个方法将工作线程标记为空闲状态,并可能让其等待新任务:
- 将工作队列的phase设为IDLE状态
- 修改全局ctl计数器,减少活动线程数
- 如果队列中没有任务,可能会让线程阻塞等待唤醒
- 返回更新后的phase,如果包含IDLE标志,表示线程应该退出
2. signalWork()
这个方法在发现有任务需要处理时调用:
- 尝试唤醒空闲工作线程或创建新工作线程
- 通过CAS操作修改ctl值来唤醒挂起的工作线程
- 如果没有空闲线程且未达到最大线程数,会创建新线程
3. topLevelExec(ForkJoinTask t, int fifo)
执行给定任务并处理局部任务:
- 调用任务的doExec()方法执行
- 之后尝试从自己的队列中获取更多任务执行(避免多次窃取操作)
- 根据fifo参数决定是FIFO还是LIFO顺序执行本地任务
复杂判断解析
窃取失败条件
b != (b = q.base) || t == null || !U.compareAndSetReference(a, k, t, null)
这个条件检查三种失败情况:
- base竞争: 在读取base后又被其他线程修改了
- 任务为空: 目标位置没有任务
- CAS失败: 尝试原子替换失败,说明其他线程已窃取该任务
可能空队列判断
a[(b + 1) & m] == null && a[(b + 2) & m] == null
如果连续三个位置都为空,队列很可能已经空了,这是一个启发式判断,避免在接近空的队列上浪费时间
信号传播条件
(prevSrc != src || nh != 0) && a[nb & m] != null
当满足以下条件时传播信号:
- 源队列改变(窃取了新队列的任务)或任务需要帮助
- 且队列中还有更多任务
这个条件确保了任务增长时会有足够的线程来处理,同时避免过多的线程唤醒
WorkQueue.push
方法实现详解
final void push(ForkJoinTask<?> task, ForkJoinPool pool, boolean internal)
这个方法的作用是将一个 ForkJoinTask
放入工作队列。它需要处理两种情况:
- 内部推送 (
internal == true
): 由队列的所有者线程(一个ForkJoinWorkerThread
)调用。这是最常见的情况,发生在任务fork
时。 - 外部推送 (
internal == false
): 由外部线程调用。这种情况下,调用push
之前必须已经通过submissionQueue
方法锁定了这个队列。
我们来分步解析其实现:
// ... (部分代码) ...
final void push(ForkJoinTask<?> task, ForkJoinPool pool, boolean internal) {
int s = top, b = base, m, cap, room; ForkJoinTask<?>[] a;
if ((a = array) != null && (cap = a.length) > 0 && // else disabled
task != null) {
int pk = task.noUserHelp() + 1; // prev slot offset
if ((room = (m = cap - 1) - (s - b)) >= 0) {
top = s + 1;
long pos = slotOffset(m & s);
if (!internal)
U.putReference(a, pos, task); // inside lock
else
U.getAndSetReference(a, pos, task); // fully fenced
if (room == 0) // resize
growArray(a, cap, s);
}
if (!internal)
unlockPhase();
if (room < 0)
throw new RejectedExecutionException("Queue capacity exceeded");
if ((room == 0 || a[m & (s - pk)] == null) &&
pool != null)
pool.signalWork(); // may have appeared empty
}
}
// ... (部分代码) ...
1. 初始化与前置检查
int s = top, b = base, m, cap, room; ForkJoinTask<?>[] a;
if ((a = array) != null && (cap = a.length) > 0 && task != null) {
// ...
}
- 读取队列的关键属性:
top
(队头,push操作的位置)、base
(队尾,steal操作的位置)、array
(任务数组)。 - 进行基本检查,确保队列已初始化且任务非空。
2. 检查容量并放置任务
if ((room = (m = cap - 1) - (s - b)) >= 0) {
top = s + 1;
long pos = slotOffset(m & s);
if (!internal)
U.putReference(a, pos, task);
else
U.getAndSetReference(a, pos, task);
if (room == 0)
growArray(a, cap, s);
}
room = (m = cap - 1) - (s - b)
: 这是计算队列剩余空间的核心。s - b
是当前队列中的任务数。m
是数组最大索引。room
大于等于0说明队列还有空间。top = s + 1
: 预先更新top
指针。这是 LIFO(后进先出)操作的关键,top
指向下一个可用的槽位。pos = slotOffset(m & s)
: 计算任务要存放的数组物理偏移量。s
是旧的top
值,所以m & s
(等价于s % cap
) 就是当前队头的位置。U.putReference
vsU.getAndSetReference
:!internal
(外部提交): 此时队列已被锁住,没有其他线程会并发修改,所以使用普通的putReference
即可。internal
(内部提交): 这是无锁操作,可能会有窃取者在并发地从base
端读取。这里使用getAndSetReference
,它提供了更强的内存屏障- 但这里依旧可能出现问题:top更新了,但task没更新(这个时间 很短)。正式因为这一点,runWorker进行工作窃取或判断null再往前判断:
- 基于实际内容:检查数组槽位是否为
null
- 不依赖 top:通过连续空槽位判断队列状态
- 保守估计:宁可多扫描也不漏掉任务
- 基于实际内容:检查数组槽位是否为
- 注意这里的判断不是说对同一个队列两种操作,而是区分两种类别的队列。对于内部队列来说可能有其它线程在窃取base。
if (room == 0) growArray(...)
: 如果push
之前队列正好是满的(room == 0
),那么在成功放入最后一个任务后,立即调用growArray
进行 扩容,为下一次push
做准备。
3. 后续处理
if (!internal)
unlockPhase();
if (room < 0)
throw new RejectedExecutionException("Queue capacity exceeded");
if ((room == 0 || a[m & (s - pk)] == null) && pool != null)
pool.signalWork();
if (!internal) unlockPhase()
: 如果是外部提交,操作完成后需要释放之前获取的锁。if (room < 0) ...
: 如果一开始计算room
就发现队列已满且无法扩容(growArray
可能会因内存不足失败),则抛出异常。pool.signalWork()
的触发条件: 这是push
方法中最精妙的部分之一。room == 0
: 如果push
之前队列是空的 (s == b
,所以room
等于cap-1
,这里判断有误,应理解为s - b == 0
时,即队列从空变为非空)。当一个空队列被放入第一个任务时,它可能需要唤醒其他正在等待任务的线程。a[m & (s - pk)] == null
: 这是一个更复杂的启发式判断。pk
是一个偏移量,s - pk
大致指向队列中较早前的一个位置。如果这个位置是null
,也可能意味着队列之前“看起来”是空的(可能被窃取空了),现在又有新任务了,也需要signalWork
。- 综合来看: 这个
if
条件的核心思想是:如果这次push
操作使得一个“看起来像是空的”队列变得“非空”,就通知整个线程池“有活干了”。这与runWorker
中窃取成功后调用signalWork
的逻辑异曲同工,都是为了尽快传播任务信息,激活整个池的算力。
总结
WorkQueue.push
方法是一个高效且线程安全的操作,其设计特点如下:
- 区分内外: 对内部(无锁)和外部(加锁)的提交采用不同的内存操作,优化性能。
- LIFO 操作: 通过移动
top
指针实现后进先出的任务存放,符合fork-join
模式的本地任务处理逻辑。 - 动态扩容: 在队列满时自动触发
growArray
,将数组容量翻倍,以应对任务的爆发式增长。 - 智能唤醒 (
signalWork
): 不仅仅是简单地把任务放进队列,还会根据队列状态判断是否需要唤醒其他空闲线程,实现了主动的负载通知机制。
这个方法与 poll
(窃取)和 nextLocalTask
(本地执行)共同构成了 WorkQueue
高效、低竞争的双端队列操作。
ForkJoinPool.common 默认池的构建过程
common池是JVM级别共享的单例ForkJoinPool,具有特殊配置和行为,而普通ForkJoinPool是为特定用途创建的独立实例。两者的底层实现相似,但common池有特殊标识和行为,尤其是不能被显式关闭,且可通过系统属性配置。
common池的初始化
common池通过静态初始化块创建:
static {
// 其他初始化...
common = new ForkJoinPool((byte)0);
}
这里使用特殊的构造函数new ForkJoinPool((byte)0)
,该构造函数是私有的,专为common池设计:
private ForkJoinPool(byte forCommonPoolOnly) {
String name = "ForkJoinPool.commonPool";
ForkJoinWorkerThreadFactory fac = defaultForkJoinWorkerThreadFactory;
UncaughtExceptionHandler handler = null;
int maxSpares = DEFAULT_COMMON_MAX_SPARES;
int pc = 0, preset = 0; // preset非0表示大小通过系统属性设置
try { // 尝试获取系统属性来覆盖默认配置
String pp = System.getProperty("java.util.concurrent.ForkJoinPool.common.parallelism");
if (pp != null) {
pc = Math.max(0, Integer.parseInt(pp));
preset = PRESET_SIZE;
}
String ms = System.getProperty("java.util.concurrent.ForkJoinPool.common.maximumSpares");
if (ms != null)
maxSpares = Math.clamp(Integer.parseInt(ms), 0, MAX_CAP);
// 加载自定义线程工厂和异常处理器
String sf = System.getProperty("java.util.concurrent.ForkJoinPool.common.threadFactory");
String sh = System.getProperty("java.util.concurrent.ForkJoinPool.common.exceptionHandler");
if (sf != null || sh != null) {
ClassLoader ldr = ClassLoader.getSystemClassLoader();
if (sf != null)
fac = (ForkJoinWorkerThreadFactory)ldr.loadClass(sf).getConstructor().newInstance();
if (sh != null)
handler = (UncaughtExceptionHandler)ldr.loadClass(sh).getConstructor().newInstance();
}
} catch (Exception ignore) {
// 忽略属性访问/解析异常
}
// 如果未通过系统属性设置,则默认为CPU核心数-1
if (preset == 0)
pc = Math.max(1, Runtime.getRuntime().availableProcessors() - 1);
int p = Math.min(pc, MAX_CAP);
int size = Math.max(MIN_QUEUES_SIZE,
(p == 0) ? 1 :
1 << (33 - Integer.numberOfLeadingZeros(p-1)));
this.parallelism = p;
this.config = ((preset & LMASK) | (((long)maxSpares) << TC_SHIFT) | (1L << RC_SHIFT));
this.factory = fac;
this.ueh = handler;
this.keepAlive = DEFAULT_KEEPALIVE;
this.saturate = null;
this.workerNamePrefix = null; // common池特有:null表示是common池
this.poolName = name;
this.queues = new WorkQueue[size];
this.container = SharedThreadContainer.create(name);
}
common池的特殊之处
-
特殊标识:
workerNamePrefix = null
用来标识这是common池 -
无法关闭:当尝试关闭common池时,会检查这个标识:
public void shutdown() { if (workerNamePrefix != null) // 不是common池才执行 tryTerminate(false, true); }
-
使用InnocuousForkJoinWorkerThread:common池使用安全线程,这些线程:
- 不属于任何用户定义的ThreadGroup
- 清除所有ThreadLocal变量
- 重置ContextClassLoader
-
默认并行度:CPU核心数-1,这样在使用common池时,留一个核心给应用程序的主线程
-
异步池启用:当需要保证异步执行时,common池会确保有至少2个工作线程:
static ForkJoinPool asyncCommonPool() { ForkJoinPool cp; int p; if ((p = (cp = common).parallelism) == 0) U.compareAndSetInt(cp, PARALLELISM, 0, 2); return cp; }
普通ForkJoinPool构造流程
当调用new ForkJoinPool()
时,会使用以下构造函数:
public ForkJoinPool() {
this(Math.min(MAX_CAP, Runtime.getRuntime().availableProcessors()),
defaultForkJoinWorkerThreadFactory, null, false,
0, MAX_CAP, 1, null, DEFAULT_KEEPALIVE, TimeUnit.MILLISECONDS);
}
这会调用最完整的构造函数:
public ForkJoinPool(int parallelism, ForkJoinWorkerThreadFactory factory,
UncaughtExceptionHandler handler, boolean asyncMode,
int corePoolSize, int maximumPoolSize, int minimumRunnable,
Predicate<? super ForkJoinPool> saturate,
long keepAliveTime, TimeUnit unit) {
// 参数校验
int p = parallelism;
if (p <= 0 || p > MAX_CAP || p > maximumPoolSize || keepAliveTime <= 0L)
throw new IllegalArgumentException();
if (factory == null || unit == null)
throw new NullPointerException();
// 计算队列数组大小(2的幂)
int size = Math.max(MIN_QUEUES_SIZE,
1 << (33 - Integer.numberOfLeadingZeros(p - 1)));
// 初始化字段
this.parallelism = p;
this.factory = factory;
this.ueh = handler;
this.saturate = saturate;
this.keepAlive = Math.max(unit.toMillis(keepAliveTime), TIMEOUT_SLOP);
// 配置位
int maxSpares = Math.clamp(maximumPoolSize - p, 0, MAX_CAP);
int minAvail = Math.clamp(minimumRunnable, 0, MAX_CAP);
this.config = (((asyncMode ? FIFO : 0) & LMASK) |
(((long)maxSpares) << TC_SHIFT) |
(((long)minAvail) << RC_SHIFT));
// 初始化队列数组
this.queues = new WorkQueue[size];
// 生成唯一的池ID和名称
String pid = Integer.toString(getAndAddPoolIds(1) + 1);
String name = "ForkJoinPool-" + pid;
this.poolName = name;
this.workerNamePrefix = name + "-worker-";
this.container = SharedThreadContainer.create(name);
}
common池与普通池的主要区别
特性 | common池 | 普通ForkJoinPool |
---|---|---|
构造方式 | new ForkJoinPool((byte)0) | new ForkJoinPool(...) |
池线程名称前缀 | null (特殊标识) | "ForkJoinPool-N-worker-" |
池名称 | "ForkJoinPool.commonPool" | "ForkJoinPool-N" |
线程类型 | InnocuousForkJoinWorkerThread | 普通ForkJoinWorkerThread |
默认并行度 | CPU核心数-1 | CPU核心数 |
可通过系统属性配置 | 是 | 否 |
可关闭 | 否 | 是 |
池ID | 不需要ID | 通过poolIds 字段递增 |
底层细节
-
队列初始化:两种池初始化时只创建队列数组,但不创建队列实例,队列在需要时才创建
-
线程创建:两种池都是懒创建工作线程,第一次提交任务时才开始创建
-
内存布局优化:
@jdk.internal.vm.annotation.Contended("fjpctl") // segregate volatile long ctl; @jdk.internal.vm.annotation.Contended("fjpctl") // colocate int parallelism;
使用
@Contended
注解避免伪共享问题 -
Unsafe操作:使用
Unsafe
类进行原子操作和内存屏障,而非VarHandles,以避免其它JDK组件的初始化依赖 -
线程池首次提交流程:
- 创建外部提交队列:
submissionQueue()
- 将任务放入队列:
q.push(task, pool, internal)
- 信号工作线程:
signalWork()
创建第一个工作线程 - 工作线程启动,调用
registerWorker()
注册到池中
- 创建外部提交队列:
-
安全线程的初始化:common池使用的
InnocuousForkJoinWorkerThread
在构造时会重置上下文类加载器和ThreadLocals
ManagedBlocker:阻塞操作与并行性管理
ManagedBlocker 解决的是 工作窃取框架中的关键矛盾:当 ForkJoinPool 中的工作线程因阻塞操作而暂停时,会降低整体并行度,影响性能。
为什么需要 ManagedBlocker ?
- 工作窃取算法效率依赖于活跃线程数:ForkJoinPool 依靠所有工作线程保持活跃来实现最优并行性
- 阻塞操作的问题:当工作线程执行阻塞操作时,会减少可用线程,影响整体并行度
- 解决方案:在检测到阻塞时,通过创建或激活额外线程来维持并行度
ManagedBlocker 接口概述
public static interface ManagedBlocker {
// 执行可能阻塞的操作,返回true表示操作完成
boolean block() throws InterruptedException;
// 检查是否可以避免阻塞,返回true表示无需阻塞
boolean isReleasable();
}
ManagedBlocker 接口包含两个关键方法:
isReleasable()
: 检查是否可以避免阻塞block()
: 执行实际的阻塞操作,成功获取资源后返回 true
block() 方法返回 boolean 值有明确意义:
- 返回 true 表示阻塞操作已完成,不需要再次阻塞
- 返回 false 表示阻塞操作未完成,需要再次尝试阻塞
ForkJoinPool 对 ManagedBlocker 的支持能力
ForkJoinPool 通过 managedBlock()
方法支持 ManagedBlocker 接口,提供以下核心能力:
- 自动补偿阻塞: 当工作线程阻塞时,自动创建或激活额外的线程来维持并行度
- 资源利用最优化: 避免过度创建线程,仅在必要时创建补偿线程
- 适应性阻塞决策: 先检查是否可避免阻塞,再决定是否需要创建补偿线程
核心实现在 ForkJoinPool.managedBlock()
方法中:
public static void managedBlock(ManagedBlocker blocker) throws InterruptedException {
Thread t; ForkJoinPool p;
if ((t = Thread.currentThread()) instanceof ForkJoinWorkerThread &&
(p = ((ForkJoinWorkerThread)t).pool) != null)
p.compensatedBlock(blocker);
else
unmanagedBlock(blocker);
}
实现分为两个关键路径:
1. ForkJoinWorkerThread 路径
当调用线程是 ForkJoinWorkerThread 时,会执行 compensatedBlock()
方法:
private void compensatedBlock(ManagedBlocker blocker) throws InterruptedException {
Objects.requireNonNull(blocker);
for (;;) {
int comp; boolean done;
long c = ctl;
if (blocker.isReleasable())
break; // 无需阻塞
if ((runState & STOP) != 0L)
throw new InterruptedException(); // 池已停止
if ((comp = tryCompensate(c)) >= 0) { // 尝试创建或激活补偿线程
try {
done = blocker.block(); // 执行阻塞操作
} finally {
if (comp > 0) // 操作完成,调整计数
getAndAddCtl(RC_UNIT);
}
if (done)
break;
}
}
}
核心补偿流程在 tryCompensate()
方法中:
private int tryCompensate(long c) {
// 尝试:
// 1. 激活等待中的线程
// 2. 创建新线程
// 3. 调整计数器
// ...根据池状态和配置执行不同策略...
return stat; // 返回补偿状态
}
tryCompensate()
方法是 ManagedBlocker 机制的核心:
- 维持并行度:它通过激活或创建额外线程来补偿即将阻塞的线程
- 高效资源使用:只在必要时创建线程,避免过度创建
- 自适应补偿:根据池状态决定是激活等待线程还是创建新线程
如果调用线程不是 ForkJoinWorkerThread,则执行简化版本:
private static void unmanagedBlock(ManagedBlocker blocker) throws InterruptedException {
Objects.requireNonNull(blocker);
do {} while (!blocker.isReleasable() && !blocker.block());
}
普通线程不需要 ManagedBlocker 接口的根本原因在于:
- 无需维护并行度:普通线程池没有工作窃取算法,不依赖精确的并行度来保持性能
- 无补偿机制:传统线程池没有设计用于高度协作的任务,阻塞不会显著影响整体性能
- 调度模型差异:ForkJoinPool 依赖所有工作线程持续运行维持效率,传统池不依赖此模型
但 ForkJoinPool 中,维持并行度是核心设计目标,一个阻塞的工作线程可能导致整个计算图的效率大幅降低,因此需要补偿机制。
tryCompensate
方法
tryCompensate
方法是 ForkJoinPool 中处理线程阻塞情况的关键机制,它在工作线程即将阻塞时尝试维持池的并行度。这一机制对于普通线程虽然不是必须的,但对于 ForkJoinPool 工作线程至关重要。
tryCompensate
方法的主要目的是在一个工作线程需要阻塞时(如等待锁或条件),保持池的整体并行度不受影响。具体来说,它尝试:
- 激活现有的空闲线程,或
- 减少活跃线程计数,或
- 创建新线程
该方法首先从 ctl
和 config
变量中解包关键字段,然后按特定优先级尝试不同的补偿策略:
1. 尝试激活空闲工作线程
if (sp != 0 && active <= pc) { // activate idle worker
// 尝试查找并激活一个空闲线程
// ...
stat = UNCOMPENSATE;
}
- 条件:当栈指针(
sp
)不为零(说明有等待的工作线程)且当前活跃线程数低于或等于并行度 - 操作:从
ctl
栈中弹出一个空闲线程,并通过unpark
激活它 - 底层原理:
sp & SMASK
计算该空闲线程在队列数组中的索引- 使用 CAS 操作修改
ctl
,将栈顶替换为下一个等待线程 - 设置被激活线程的
phase
(用于标识状态) - 如果线程在等待(
parking != 0
),则唤醒它
这是最优先的策略,因为它复用已有线程,开销最小。
2. 减少活跃线程计数
else if (active > minActive && total >= pc) { // reduce active workers
// 减少 RC 计数
stat = UNCOMPENSATE;
}
- 条件:当活跃线程数超过最小活跃数且总线程数不低于并行度
- 操作:原子减少
ctl
中的活跃线程计数(RC_UNIT) - 底层原理:
- 这种情况表明池中有足够的线程,可以安全地减少一个活跃计数
- 减少计数而不是真正销毁线程,使已有线程可以继续工作
这是第二优先级策略,适用于资源足够时的简单统计调整。
3. 尝试扩展线程池
else if (total < maxTotal && total < MAX_CAP) { // try to expand pool
// 增加 TC 计数并创建新线程
stat = createWorker() ? UNCOMPENSATE : 0;
}
- 条件:当总线程数小于最大限制且小于系统上限
- 操作:原子增加
ctl
中的总线程数(TC_UNIT),然后创建新工作线程 - 底层原理:
- 首先检查池是否正在终止
- 通过 CAS 增加总线程计数
- 调用
createWorker()
实际创建并启动新线程 - 返回值取决于线程创建是否成功
当前两种策略不适用时,这是第三优先级策略。
4. 处理竞争与限制情况
else if (!compareAndSetCtl(c, c)) // validate
; // retry
else if ((sat = saturate) != null && sat.test(this))
stat = 0;
else
throw new RejectedExecutionException(...);
- 上下文:当前述三种策略都不适用时
- 操作:
- 首先验证
ctl
没有变化 - 如果配置了
saturate
谓词且测试通过,则返回 0 - 否则抛出异常,表示达到了线程限制
- 首先验证
这部分处理边界情况,确保要么能成功返回,要么明确拒绝。
返回值意义
UNCOMPENSATE
(0x10000):成功补偿,调用者需要在后续解除阻塞时调整计数0
:可以阻塞,但不需要后续调整-1
:由于竞争失败,调用者应重试
补偿实现的关键技术
- 原子操作:所有关键操作如修改
ctl
都使用 CAS 确保并发安全 - 位操作:使用位移和掩码从压缩字段中提取和修改值
- 分级策略:优先级从激活空闲线程→减少计数→创建新线程→拒绝
- 队列结构:利用
ctl
作为 Treiber 栈记录和管理空闲线程
ManagedBlocker 典型实现示例
1. 锁等待实现
class ManagedLocker implements ForkJoinPool.ManagedBlocker {
final ReentrantLock lock;
boolean hasLock = false;
ManagedLocker(ReentrantLock lock) { this.lock = lock; }
public boolean block() {
if (!hasLock)
lock.lock();
return true;
}
public boolean isReleasable() {
return hasLock || (hasLock = lock.tryLock());
}
}
2. 阻塞队列操作
class QueueTaker<E> implements ForkJoinPool.ManagedBlocker {
final BlockingQueue<E> queue;
volatile E item = null;
QueueTaker(BlockingQueue<E> q) { this.queue = q; }
public boolean block() throws InterruptedException {
if (item == null)
item = queue.take();
return true;
}
public boolean isReleasable() {
return item != null || (item = queue.poll()) != null;
}
public E getItem() { // 在 managedBlock 完成后调用
return item;
}
}
3. CompletableFuture 中的使用
Java 标准库中 CompletableFuture 使用了 ManagedBlocker 来管理其阻塞操作:
static final class Signaller extends Completion implements ForkJoinPool.ManagedBlocker {
long nanos;
final long deadline;
final boolean interruptible;
volatile Thread thread;
public boolean isReleasable() {
if (Thread.interrupted())
interrupted = true;
return ((interrupted && interruptible) ||
(deadline != 0L &&
(nanos <= 0L ||
(nanos = deadline - System.nanoTime()) <= 0L)) ||
thread == null);
}
public boolean block() {
while (!isReleasable()) {
if (deadline == 0L)
LockSupport.park(this);
else
LockSupport.parkNanos(this, nanos);
}
return true;
}
}
ManagedBlocker 工作原理总结
-
补偿机制:
- ForkJoinPool 维护活动线程计数 (ctl 中的 RC_MASK 部分)
- 当工作线程阻塞时,尝试激活等待线程或创建新线程
-
决策过程:
compensatedBlock() ↓ isReleasable() → true → 立即返回 ↓ (false) tryCompensate() → 创建或激活线程 ↓ block() → 执行阻塞操作 ↓ 调整线程计数
-
优势:
- 避免线程池饥饿: 阻塞不会减少线程池的有效并行度
- 按需创建线程: 只在必要时才创建额外线程
- 支持复杂同步: 允许在 ForkJoinPool 中使用需要同步的操作
-
适用场景:
- I/O 操作
- 数据库访问
- 外部服务调用
- 条件变量等待
- 资源获取和锁定
ManagedBlocker 是一种强大的机制,允许 ForkJoinPool 在保持并行性的同时处理阻塞操作,使工作窃取算法能够适应更广泛的应用场景。
shutdown
和 tryTerminate
函数
shutdown
函数用于启动有序关闭过程,即不再接受新的任务,但会继续执行已提交的任务。这个函数的实现非常简单,只调用了一个私有方法 tryTerminate
。
public void shutdown() {
if (workerNamePrefix != null) // not common pool
tryTerminate(false, true);
}
tryTerminate
函数通过检查和更新 runState
字段来管理 ForkJoinPool
的终止过程。它根据传入的参数决定是否立即终止池或等待所有任务完成后再终止。该函数确保在终止过程中,所有任务都被清理,所有工作线程都被中断,并且池的状态正确更新。
以下是该函数的详细解释:
参数
-
now
:如果为true
,则无条件终止池;否则,只有在没有任务和没有活动工作线程时才终止。 -
enable
:如果为true
,则在下一个可能的机会终止池。
返回值
-
返回
runState
的当前值。
函数流程
-
检查是否已终止:
if (((e = runState) & TERMINATED) != 0L) now = false;
如果池已经终止,则将
now
设置为false
。 -
检查是否已停止:
else if ((e & STOP) != 0L) now = true;
如果池已经停止,则将
now
设置为true
。 -
立即终止:
else if (now) { if (((ps = getAndBitwiseOrRunState(SHUTDOWN|STOP) & STOP)) == 0L) { if ((ps & RS_LOCK) != 0L) { spinLockRunState(); // ensure queues array stable after stop unlockRunState(); } interruptAll(); } }
如果
now
为true
,则尝试设置SHUTDOWN
和STOP
状态,并中断所有工作线程。 -
条件终止:
else if ((isShutdown = (e & SHUTDOWN)) != 0L || enable) { long quiet; DelayScheduler ds; if (isShutdown == 0L) getAndBitwiseOrRunState(SHUTDOWN); if ((quiet = quiescent()) > 0) now = true; else if (quiet == 0 && (ds = delayScheduler) != null) ds.signal(); }
如果池已关闭或
enable
为true
,则检查池是否静止,如果静止则将now
设置为true
,否则信号延迟调度器。 -
终止过程:
if (now) { DelayScheduler ds; releaseWaiters(); if ((ds = delayScheduler) != null) ds.signal(); for (;;) { if (((e = runState) & CLEANED) == 0L) { boolean clean = cleanQueues(); if (((e = runState) & CLEANED) == 0L && clean) e = getAndBitwiseOrRunState(CLEANED) | CLEANED; } if ((e & TERMINATED) != 0L) break; if (ctl != 0L) // else loop if didn't finish cleaning break; if ((ds = delayScheduler) != null && ds.signal() >= 0) break; if ((e & CLEANED) != 0L) { e |= TERMINATED; if ((getAndBitwiseOrRunState(TERMINATED) & TERMINATED) == 0L) { CountDownLatch done; SharedThreadContainer ctr; if ((done = termination) != null) done.countDown(); if ((ctr = container) != null) ctr.close(); } break; } } }
如果
now
为true
,则释放等待的工作线程,信号延迟调度器,并开始清理队列。如果所有队列都已清理,则设置TERMINATED
状态并关闭池。
spinLockRunState
函数
spinLockRunState
是一个自旋锁实现,用于确保在多线程环境下安全地更新 runState
字段。
private long spinLockRunState() {
for (int waits = 0;;) {
long s, u;
if (((s = runState) & RS_LOCK) == 0L) {
if (casRunState(s, u = s + RS_LOCK))
return u;
waits = 0;
} else if (waits < SPIN_WAITS) {
++waits;
Thread.onSpinWait();
} else {
if (waits < MIN_SLEEP)
waits = MIN_SLEEP;
LockSupport.parkNanos(this, (long)waits);
if (waits < MAX_SLEEP)
waits <<= 1;
}
}
}
它通过不断尝试设置 RS_LOCK
位来获取锁,直到成功为止。如果自旋次数超过一定阈值,它会短暂地休眠以减少 CPU 消耗。
spinLockRunState
在 ForkJoinPool
中主要用于以下几个地方:
tryTerminate
函数:在终止池的过程中,确保runState
字段的安全更新。lockRunState
函数:在需要锁定runState
字段时使用,确保操作的原子性。tryRemoveAndExec
函数:在尝试移除并执行任务时,确保对runState
字段的安全访问。
shutdown
函数通过调用 tryTerminate
函数来启动池的有序关闭过程。tryTerminate
函数根据传入的参数决定是否立即终止池或等待所有任务完成后再终止。spinLockRunState
是一个自旋锁实现,用于确保在多线程环境下安全地更新 runState
字段。它在 ForkJoinPool
中的多个地方使用,以确保操作的原子性和安全性。
runState
是 ForkJoinPool
类中的一个关键字段,用于表示池的运行状态。它是一个 64 位的 long 类型字段,其中不同的位表示不同的状态和配置信息。以下是 runState
的各个取值及其含义:
-
STOP (1L << 0):表示池正在终止。当池处于此状态时,所有任务都将停止执行,工作线程将被中断。
-
SHUTDOWN (1L << 1):表示池已关闭,不再接受新的任务,但会继续执行已提交的任务。当池处于此状态时,一旦所有任务都完成,池将进入终止状态。
-
CLEANED (1L << 2):表示池已停止,并且所有未执行的任务都已被取消。当池处于此状态时,队列已被清理。
-
TERMINATED (1L << 3):表示池已终止。当池处于此状态时,所有工作线程都已注销,所有队列都已被清理。
-
RS_LOCK (1L << 4):表示
runState
字段已被锁定。这是为了确保在多线程环境下对runState
字段的安全更新。
runState
字段在 ForkJoinPool
的多个地方使用,主要用于管理池的状态和同步操作。以下是一些关键的使用场景:
-
终止池:在
tryTerminate
方法中,runState
字段用于控制池的终止过程。根据传入的参数,该方法决定是否立即终止池或等待所有任务完成后再终止。 -
锁定和解锁:在
lockRunState
和unlockRunState
方法中,runState
字段用于锁定和解锁。这些方法确保在多线程环境下对runState
字段的安全更新。 -
状态检查:在
isShutdown
,isTerminating
, 和isTerminated
方法中,runState
字段用于检查池的当前状态。 -
自旋锁:在
spinLockRunState
方法中,runState
字段用于实现自旋锁。这个自旋锁确保在多线程环境下对runState
字段的安全更新。
语义是:需短时间独占访问 queues
或全局状态的代码路径(如线程注册、终止、队列扩容)。
ForkJoinPool 的调度能力与任务超时机制
ForkJoinPool 不仅提供了工作窃取算法实现并行计算,还增加了调度功能,使其能够作为 ScheduledExecutorService 的实现。这些功能丰富了 ForkJoinPool 的应用场景,让其能够处理定时任务和超时任务。
1. 调度功能的实现架构
ForkJoinPool → DelayScheduler → ScheduledForkJoinTask
ForkJoinPool 的调度功能通过以下组件实现:
- DelayScheduler:独立线程,管理所有延迟任务
- ScheduledForkJoinTask:具有延迟执行能力的特殊 ForkJoinTask
private DelayScheduler startDelayScheduler() {
DelayScheduler ds;
if ((ds = delayScheduler) == null) {
// 创建并启动调度器线程
// ...
}
return ds;
}
- DelayScheduler 是一个单独线程,可以懒加载方式初始化
- 通过小根堆维护所有延迟任务
- 使用类似 Timer 的算法,但支持大规模并发和更好的线程管理
2. 三类调度方法
一次性延迟执行
public ScheduledFuture<?> schedule(Runnable command, long delay, TimeUnit unit)
-
在指定延迟后执行一次任务
固定速率执行
public ScheduledFuture<?> scheduleAtFixedRate(Runnable command,
long initialDelay,
long period, TimeUnit unit)
-
以固定间隔重复执行任务
-
计时从上次开始执行时计算
固定延迟执行
public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command,
long initialDelay,
long delay, TimeUnit unit)
- 任务执行完成后等待指定时间再次执行
- 计时从上次执行完成时计算
这三种方法都是通过创建 ScheduledForkJoinTask 并提交到 DelayScheduler 来实现的:
return scheduleDelayedTask(
new ScheduledForkJoinTask<Void>(
unit.toNanos(initialDelay), unit.toNanos(period),
false, Objects.requireNonNull(command), null, this));
3. TimeoutAction 与任务超时机制
ForkJoinPool 提供了一个强大的超时任务机制:
public <V> ForkJoinTask<V> submitWithTimeout(Callable<V> callable,
long timeout, TimeUnit unit,
Consumer<? super ForkJoinTask<V>> timeoutAction)
TimeoutAction 的设计
static final class TimeoutAction<V> implements Runnable {
ForkJoinTask.CallableWithTimeout<V> task;
Consumer<? super ForkJoinTask<V>> action;
public void run() {
if (t != null && t.status >= 0) {
if (a == null)
t.cancel(true);
else {
a.accept(t);
t.interruptIfRunning(true);
}
}
}
}
TimeoutAction 有以下特点:
- 是一个 Runnable 包装,在任务超时时执行
- 可以取消任务或应用定制行为
- 允许两种超时处理策略:
- 默认取消策略:
cancel(true)
- 自定义策略:执行 Consumer 操作后中断任务
- 默认取消策略:
submitWithTimeout 的实现
public <V> ForkJoinTask<V> submitWithTimeout(...) {
// 创建超时处理器
TimeoutAction<V> onTimeout = new TimeoutAction<V>(timeoutAction);
// 创建定时任务来触发超时
ScheduledForkJoinTask<Void> timeoutTask = new ScheduledForkJoinTask<>(...);
// 创建实际任务,并与超时任务关联
onTimeout.task = task = new CallableWithTimeout<V>(callable, timeoutTask);
// 提交定时超时任务
scheduleDelayedTask(timeoutTask);
// 提交实际任务
return poolSubmit(true, task);
}
4. 实现中的亮点设计
1. 懒加载的调度器
final <T> ScheduledForkJoinTask<T> scheduleDelayedTask(ScheduledForkJoinTask<T> task) {
DelayScheduler ds;
if (((ds = delayScheduler) == null && (ds = startDelayScheduler()) == null) ||
(runState & SHUTDOWN) != 0L)
throw new RejectedExecutionException();
ds.pend(task);
return task;
}
- 只有在首次需要时才创建调度器线程
- 减少不需要调度功能时的资源使用
2. 取消策略处理
public void cancelDelayedTasksOnShutdown() {
DelayScheduler ds;
if ((ds = delayScheduler) != null || (ds = startDelayScheduler()) != null)
ds.cancelDelayedTasksOnShutdown();
}
- 允许在关闭池时取消所有未执行的延迟任务
- 为定时任务提供优雅关闭策略
3. 与工作窃取模型的整合
final void executeEnabledScheduledTask(ScheduledForkJoinTask<?> task) {
externalSubmissionQueue(false).push(task, this, false);
}
- 延迟任务到期后,通过外部提交队列进入 ForkJoinPool
- 复用工作窃取算法的所有优势,包括自适应线程管理和负载均衡
- 定时任务和普通任务共享线程池资源
4. 自定义超时行为
submitWithTimeout 方法的 timeoutAction 参数提供了处理超时的灵活性:
1. **自定义完成值**:`timeoutAction = t -> t.complete(defaultValue)`
2. **抛出特定异常**:`timeoutAction = t -> t.completeExceptionally(new TimeoutException())`
3. **执行降级策略**:`timeoutAction = t -> executeBackupTask()`