在ForkJoin框架概述中已经对ForkJoinTask进行了基本介绍,本文主要从实现的角度剖析ForkJoinTask。简单的说,ForkJoinTask将任务fork成足够小的任务,并发解决这些小任务,然后将这些小任务结果join。这种思想充分利用了CPU的多核系统,使得CPU的利用率得到大幅度提升,减少了任务执行时间。
通常我们会利用ForkJoinTask的fork方法来分割任务,利用join方法来合并任务,因此我们首先以这两个方法作为切入点。
public final ForkJoinTask fork() {
((ForkJoinWorkerThread) Thread.currentThread()).pushTask(this);
return this;
}
该方法很简单,将该任务添加到当前线程维护的队列队首处,返回该任务。这里有三点需要注意的:
假设我们将当前任务t分割成两个任务t1和t2,为了获取任务t的结果,需要等待任务t1和t2的结果,代码片断通常是这种形式:
t1.fork();
t2.fork();
result = t1.join() + t2.join();
join方法正是阻塞等待当前任务执行结束并返回结果。如果任务非正常结束,join方法可能会抛出异常。join结果的时候,如果线程维护的队列头就是该任务,那么直接执行该任务,否则还有更小的任务需要执行,等待该线程执行完该任务。
和很多其他JUC框架类似,ForkJoinTask也有自己的任务执行状态,先学习下ForkJoinTask的几个状态:
volatile int status;
private static final int NORMAL = -1;
private static final int CANCELLED = -2;
private static final int EXCEPTIONAL = -3;
private static final int SIGNAL = 1;
status是一个volatile变量,表示当前任务的执行状态,它有五个状态,负数表示该任务已经执行完成,非负数表示任务还没有执行完成。其中已经执行完成状态又包括NORMAL、CANCELLED、EXCEPTIONAL三种状态,未完成状态包括初始状态0和SIGNAL。下面详细看下每个状态:
join方法调用了doJoin方法,doJoin方法有一个关键代码片断:
//任务正常完成,设置正常完成状态,通知其他需要join该任务的线程
if (completed)
return setCompletion(NORMAL);
当前任务完成后,设置该任务的完成状态为NORMAL,并且notifyAll(唤醒)其他在该任务上等待的线程。其他线程被唤醒后会合并该任务的执行结果。既然有notifyAll,那对应的wait在哪里呢?ForkJoinPool调用了ForkJoinTask的tryAwaitDone方法,等待任务完成。
接着详细看下doJoin方法:
private int doJoin() {
Thread t; ForkJoinWorkerThread w; int s; boolean completed;
//如果当前线程是ForkJoinWorkerThread
if ((t = Thread.currentThread()) instanceof ForkJoinWorkerThread) {
//负数表示任务已经完成了,直接返回
if ((s = status) < 0)
return s;
//从队首取出当前任务,如果队首不是该任务,unpushTask返回false
if ((w = (ForkJoinWorkerThread)t).unpushTask(this)) {
try {
//执行任务,exec是一个抽象方法,为了扩展,留给子类实现
//exec返回任务是否正常完成
completed = exec();
} catch (Throwable rex) {
//任务执行抛出异常,设置任务异常完成状态
return setExceptionalCompletion(rex);
}
//任务正常完成,设置正常完成状态,通知其他需要join该任务的线程
if (completed)
return setCompletion(NORMAL);
}
//等待当前任务完成
return w.joinTask(this);
}
//当前任务不是由ForkJoinWorkThread线程执行的,等待它执行完成
else
return externalAwaitDone();
}
doJoin方法等待该任务完成,返回完成时的状态(NORMAL、CANCELLED、EXCEPTIONAL)。
如果执行当前任务的线程不是ForkJoinWorkerThread,调用externalAwaitDone方法等待任务执行完成。否则,从当前线程维护的队列取队首的任务,如果队首的任务不是当前的任务或者任务未完成,调用当前线程的joinTask方法将当前任务加入到等待队列并等待该任务执行完成。
exec是一个抽象方法,完成具体任务的代码,由子类实现,该方法返回任务是否正常完成。任务执行过程如果抛出异常,捕获异常并设置异常完成状态。如果任务正常完成,设置正常状态并通知其他需要join该任务的线程,其他需要join该任务的线程通常是一个等待父任务完成的线程,也就是说,此时当前任务其实是个子任务,子任务结束后,父任务就可以尝试合并子任务的执行结果了,看下示例图:
任务执行过程抛出异常,调用setExceptionalCompletion方法设置异常完成状态,学习该方法源码之前我们先学习下ForkJoinTask的类变量exceptionTable和exceptionTableLock,内部静态类ExceptionNode:
//任务执行过程中抛出异常的ForkJoinTask弱引用数组,这是一个类变量,所有ForkJoinTask共用
//该数组
private static final ExceptionNode[] exceptionTable;
//异常任务table锁
private static final ReentrantLock exceptionTableLock;
//保存弱引用的queue,GC时将回收的对象对应的弱引用保存到该队列中
private static final ReferenceQueue
任务执行过程抛出异常时,调用者可以获取该异常,ForkJoinTask并没有直接将异常的任务保存起来,而是保存了异常任务的弱引用,在合适的时候,GC将会回收该异常任务,被回收对象对应的弱引用将会保存在弱引用队列中。
private int setExceptionalCompletion(Throwable ex) {
//System.identityHashCode和Object.hashCode返回的值一样,
//都是根据对象在内存中的地址计算出来的哈希码
int h = System.identityHashCode(this);
//操作异常任务表之前先获取锁
final ReentrantLock lock = exceptionTableLock;
lock.lock();
try {
//删除已经被回收对象对应的弱引用,该方法会遍历exceptionTableRefQueue,并删除exceptionTable中
//对应的弱引用
expungeStaleExceptions();
ExceptionNode[] t = exceptionTable;
//将执行过程抛出异常的任务弱引用保存到exceptionTable,这里其实是将
//exceptionTable当作哈希表使用,i就是保存的位置
int i = h & (t.length - 1);
//遍历哈希表索引i处的链表,如果遍历过程中发现已经存在该任务,跳出循环,否则
//遍历到链表末尾时,创建新的ExceptionNode,并将该节点放到链表的头部
for (ExceptionNode e = t[i]; ; e = e.next) {
if (e == null) {
t[i] = new ExceptionNode(this, ex, t[i]);
break;
}
if (e.get() == this)
break;
}
} finally {
lock.unlock();
}
//设置任务的完成状态为EXCEPTIONAL
return setCompletion(EXCEPTIONAL);
}
方法的注释已经很清楚了,这里再总结下该方法的处理逻辑:
回到doJoin方法,当任务正常完成时,调用setCompletion方法将任务完成状态设置为NORMAL,接着看下setCompletion源码:
//设置任务完成状态,completion可选项为:NORMAL、CANCELLED、EXCEPTIONAL
private int setCompletion(int completion) {
for (int s;;) {
//状态值为负数,说明任务已经完成
if ((s = status) < 0)
return s;
//原子地设置状态值
if (UNSAFE.compareAndSwapInt(this, statusOffset, s, completion)) {
//如果原状态为SIGNAL,通知其他在该任务上等待的线程join该任务的结果
if (s != 0)
synchronized (this) { notifyAll(); }
return completion;
}
}
}
setCompletion方法也很简单,如果任务状态已经完成,直接返回当前任务完成状态。否则原子地设置状态值为completion,如果设置成功并且原状态为SIGNAL,需要唤醒其他在该任务等待的线程。
我们知道,ForkJoinTask实现了Future接口,接下来我们看下ForkJoinTask对这些接口的实现,先看下get方法的实现。
get方法等待任务执行完成并返回任务计算结果,看下源码:
public final V get() throws InterruptedException, ExecutionException {
int s = (Thread.currentThread() instanceof ForkJoinWorkerThread) ?
doJoin() : externalInterruptibleAwaitDone(0L);
Throwable ex;
if (s == CANCELLED)
throw new CancellationException();
if (s == EXCEPTIONAL && (ex = getThrowableException()) != null)
throw new ExecutionException(ex);
return getRawResult();
}
如果当前线程是ForkJoinWorkerThread,调用doJoin方法获取结果,该方法前面已经讲过了。如果当前线程不是ForkerJoinWorkerThread,调用externalInterruptibleAwaitDone方法。
任务执行完成返回后,如果任务完成状态是CANCELLED,抛出CancellationException异常。如果任务完成状态是EXCEPTIONAL,将任务执行过程中抛出的异常包装成ExecutionExcepiton重新抛出。
getRawResult是一个抽象方法,留给子类实现。
重点看下getThrowableException方法,该方法返回当前任务执行过程中抛出的异常,看下源码:
private Throwable getThrowableException() {
//如果任务状态不是EXCEPTIONAL,返回null
if (status != EXCEPTIONAL)
return null;
int h = System.identityHashCode(this);
ExceptionNode e;
final ReentrantLock lock = exceptionTableLock;
lock.lock();
try {
expungeStaleExceptions();
ExceptionNode[] t = exceptionTable;
e = t[h & (t.length - 1)];
//从哈希表exceptionTable中找到当前任务抛出的异常
while (e != null && e.get() != this)
e = e.next;
} finally {
lock.unlock();
}
Throwable ex;
if (e == null || (ex = e.ex) == null)
return null;
//如果该异常不是由当前线程抛出的,通过该异常的无参构造函数,或者只有一个Throwable参数的构造函数
//新建一个异常并返回
if (e.thrower != Thread.currentThread().getId()) {
Class ec = ex.getClass();
try {
Constructor> noArgCtor = null;
Constructor>[] cs = ec.getConstructors();
for (int i = 0; i < cs.length; ++i) {
Constructor> c = cs[i];
Class>[] ps = c.getParameterTypes();
if (ps.length == 0)
noArgCtor = c;
else if (ps.length == 1 && ps[0] == Throwable.class)
return (Throwable)(c.newInstance(ex));
}
if (noArgCtor != null) {
Throwable wx = (Throwable)(noArgCtor.newInstance());
wx.initCause(ex);
return wx;
}
} catch (Exception ignore) {
}
}
return ex;
}
该方法从异常表exceptionTable中取当前任务抛出的异常。如果抛出该异常的不是当前线程,查找该异常类对应的无参构造函数、或者只有一个参数Throwable的构造函数,通过该构造函数和反射,创建一个该异常的实例并返回。
public boolean cancel(boolean mayInterruptIfRunning) {
return setCompletion(CANCELLED) == CANCELLED;
}
该方法很简单,调用setCompletion方法将任务的状态设置为CANCELLED,如果设置成功说明取消任务成功,否则取消任务失败。
该方法等待任务执行完成,返回任务执行结果,或者抛出异常。
public final V invoke() {
if (doInvoke() != NORMAL)
//非正常完成结果,调用reportResult,该方法可能会重新抛出异常
return reportResult();
else
//返回任务执行结果
return getRawResult();
}
//执行任务,任务完成时返回任务完成状态
private int doInvoke() {
int s; boolean completed;
if ((s = status) < 0)
return s;
try {
completed = exec();
} catch (Throwable rex) {
return setExceptionalCompletion(rex);
}
if (completed)
return setCompletion(NORMAL);
else
return doJoin();
}
ForkJoinTask有三个invokeAll的重载方法,先看下最简单的一个重载方法:
//执行任务t1和t2
public static void invokeAll(ForkJoinTask> t1, ForkJoinTask> t2) {
//先将t2加入到任务队列
t2.fork();
//执行t1
t1.invoke();
//等待t2执行完成
t2.join();
}
这个invokeAll方法执行两个任务t1和t2并等待这两个任务执行完成才返回。首先将t2加入到当前线程维护的队列,等待被调度,这个过程是异步的,加入到队列后就返回。其次执行任务t1,注意t1由当前线程执行。最后等待t2执行完成。
拉着看下另一个更加复杂的invokeAll方法:
//该方法执行集合中的任务,集合中第一个任务由当前线程直接执行,其他任务加入到当前线程维护的队列
public static void invokeAll(ForkJoinTask>... tasks) {
Throwable ex = null;
int last = tasks.length - 1;
for (int i = last; i >= 0; --i) {
ForkJoinTask> t = tasks[i];
if (t == null) {
if (ex == null)
ex = new NullPointerException();
}
//除了第一个任务外的其他任务加入到等待队列
else if (i != 0)
t.fork();
//第一个任务直接执行
else if (t.doInvoke() < NORMAL && ex == null)
ex = t.getException();
}
//等待任务完成
for (int i = 1; i <= last; ++i) {
ForkJoinTask> t = tasks[i];
if (t != null) {
if (ex != null)
t.cancel(false);
else if (t.doJoin() < NORMAL && ex == null)
ex = t.getException();
}
}
if (ex != null)
UNSAFE.throwException(ex);
}
get方法等待任务执行完成,并返回任务执行结果。
public final V get() throws InterruptedException, ExecutionException {
//等待任务结束,返回任务结束的状态
int s = (Thread.currentThread() instanceof ForkJoinWorkerThread) ?
doJoin() : externalInterruptibleAwaitDone(0L);
Throwable ex;
//任务被取消了,重新抛出异常
if (s == CANCELLED)
throw new CancellationException();
//任务执行过程抛出异常,将该异常重新抛出
if (s == EXCEPTIONAL && (ex = getThrowableException()) != null)
throw new ExecutionException(ex);
//取任务执行结果,抽象方法,由子类实现
return getRawResult();
}
该方法主要调用doJoin方法等待任务结束,根据任务结束的状态,决定抛出相应的异常,或者返回任务结果。
reinitialize方法重置任务的状态使得该任务可以被重新执行。
public void reinitialize() {
if (status == EXCEPTIONAL)
clearExceptionalCompletion();
else
status = 0;
}
该方法重置任务状态为0,如果有异常信息,清除异常信息。
这两个方法非常简单,getPool返回执行该任务线程所在的线程池,inForkJoinPool返回该任务是否由FJ线程执行。
public static ForkJoinPool getPool() {
Thread t = Thread.currentThread();
return (t instanceof ForkJoinWorkerThread) ?
((ForkJoinWorkerThread) t).pool : null;
}
public static boolean inForkJoinPool() {
return Thread.currentThread() instanceof ForkJoinWorkerThread;
}
tryUnfork方法尝试将该任务从任务队列中弹出。该任务不再被线程池调度。
public boolean tryUnfork() {
return ((ForkJoinWorkerThread) Thread.currentThread())
.unpushTask(this);
}
getQueuedTaskCount方法返回当前线程已经fork但是没有执行的任务数量。
public static int getQueuedTaskCount() {
//返回任务队列中任务的数量
return ((ForkJoinWorkerThread) Thread.currentThread())
.getQueueSize();
}
好了,ForkJoinTask的主要源码分析到此为上。