Java并发包线程池之ForkJoinPool即ForkJoin框架(一)
前言
这是Java并发包提供的最后一个线程池实现,也是最复杂的一个线程池。针对这一部分的代码太复杂,由于目前理解有限,只做简单介绍。通常大家说的Fork/Join框架其实就是指由ForkJoinPool作为线程池、ForkJoinTask(通常实现其三个抽象子类)为任务、ForkJoinWorkerThread作为执行任务的具体线程实体这三者构成的任务调度机制。通俗的说,ForkJoin框架的作用主要是为了实现将大型复杂任务进行递归的分解,直到任务足够小才直接执行,从而递归的返回各个足够小的任务的结果汇集成一个大任务的结果,依次类推最终得出最初提交的那个大型复杂任务的结果,这和方法的递归调用思想是一样的。当然ForkJoinPool线程池为了提高任务的并行度和吞吐量做了非常多而且复杂的设计实现,其中最著名的就是任务窃取机制。
对照前面介绍的ThreadPoolExecutor执行的任务是Future的实现类FutureTask、执行线程的实体是内部类Worker,ForkJoinPool执行的任务就是Future的实现类ForkJoinTask、执行线程就是ForkJoinWorkerThread。
ForkJoinWorkerThread
该类直接继承了Thread,但是仅仅是为了增加一些额外的功能,并没有对线程的调度执行做任何更改。ForkJoinWorkerThread是被ForkJoinPool管理的工作线程,在创建出来之后都被设置成为了守护线程,由它来执行ForkJoinTasks。该类主要为了维护创建线程实例时通过ForkJoinPool为其创建的任务队列,与其他两个线程池整个线程池只有一个任务队列不同,ForkJoinPool管理的所有工作线程都拥有自己的工作队列,为了实现任务窃取机制,该队列被设计成一个双端队列,而ForkJoinWorkerThread的首要任务就是执行自己的这个双端任务队列中的任务,其次是窃取其他线程的工作队列,以下是其代码片段:
public class ForkJoinWorkerThread extends Thread { final ForkJoinPool pool; // 这个线程工作的ForkJoinPool池
final ForkJoinPool.WorkQueue workQueue; // 这个线程拥有的工作窃取机制的工作队列 //创建在给定ForkJoinPool池中执行的ForkJoinWorkerThread。
protected ForkJoinWorkerThread(ForkJoinPool pool) {
// Use a placeholder until a useful name can be set in registerWorker
super("aForkJoinWorkerThread");
this.pool = pool;
this.workQueue = pool.registerWorker(this); //向ForkJoinPool执行池注册当前工作线程,ForkJoinPool为其分配一个工作队列
} //该工作线程的执行内容就是执行工作队列中的任务
public void run() {
if (workQueue.array == null) { // only run once
Throwable exception = null;
try {
onStart();
pool.runWorker(workQueue); //执行工作队列中的任务
} catch (Throwable ex) {
exception = ex; //记录异常
} finally {
try {
onTermination(exception);
} catch (Throwable ex) {
if (exception == null)
exception = ex;
} finally {
pool.deregisterWorker(this, exception); //撤销工作
}
}
}
} .....
}
ForkJoinTask
与FutureTask一样, ForkJoinTask也是Future的子类,不过它是一个抽象类,其实现过程中与ForkJoinPool相互交叉,因此其源码在不理解ForkJoinPool的情况下很难全部看明白,这里只了解大概,ForkJoinTask的作用就是根据任务的分解实现(exec抽象方法),将任务进行拆分,并等待子任务的执行结果,由此可以组合成父任务的结果,以此类推。
ForkJoinTask有一个int类型的status字段,其高16位存储任务执行状态例如NORMAL、CANCELLED或EXCEPTIONAL,低16位预留用于用户自定义的标记。任务未完成之前status大于等于0,完成之后就是NORMAL、CANCELLED或EXCEPTIONAL这几个小于0的值,这几个值也是按大小顺序的:0(初始状态) > NORMAL > CANCELLED > EXCEPTIONAL.
public abstract class ForkJoinTask<V> implements Future<V>, Serializable { /** 该任务的执行状态 */
volatile int status; // accessed directly by pool and workers
static final int DONE_MASK = 0xf0000000; // mask out non-completion bits
static final int NORMAL = 0xf0000000; // must be negative
static final int CANCELLED = 0xc0000000; // must be < NORMAL
static final int EXCEPTIONAL = 0x80000000; // must be < CANCELLED
static final int SIGNAL = 0x00010000; // must be >= 1 << 16
static final int SMASK = 0x0000ffff; // short bits for tags // 异常哈希表 //被任务抛出的异常数组,为了报告给调用者。因为异常很少见,所以我们不直接将它们保存在task对象中,而是使用弱引用数组。注意,取消异常不会出现在数组,而是记录在statue字段中
//注意这些都是 static 类属性,所有的ForkJoinTask共用的。
private static final ExceptionNode[] exceptionTable; //异常哈希链表数组
private static final ReentrantLock exceptionTableLock;
private static final ReferenceQueue<Object> exceptionTableRefQueue; //在ForkJoinTask被GC回收之后,相应的异常节点对象的引用队列 /**
* 固定容量的exceptionTable.
*/
private static final int EXCEPTION_MAP_CAPACITY = 32; //异常数组的键值对节点。
//该哈希链表数组使用线程id进行比较,该数组具有固定的容量,因为它只维护任务异常足够长,以便参与者访问它们,所以在持续的时间内不应该变得非常大。但是,由于我们不知道最后一个joiner何时完成,我们必须使用弱引用并删除它们。我们对每个操作都这样做(因此完全锁定)。此外,任何ForkJoinPool池中的一些线程在其池变为isQuiescent时都会调用helpExpungeStaleExceptions
static final class ExceptionNode extends WeakReference<ForkJoinTask<?>> {
final Throwable ex;
ExceptionNode next;
final long thrower; // 抛出异常的线程id
final int hashCode; // 在弱引用消失之前存储hashCode
ExceptionNode(ForkJoinTask<?> task, Throwable ex, ExceptionNode next) {
super(task, exceptionTableRefQueue); //在ForkJoinTask被GC回收之后,会将该节点加入队列exceptionTableRefQueue
this.ex = ex;
this.next = next;
this.thrower = Thread.currentThread().getId();
this.hashCode = System.identityHashCode(task);
}
} .................
}
除了status记录任务的执行状态之外,其他字段主要是为了对任务执行的异常的处理,ForkJoinTask采用了哈希数组 + 链表的数据结构(JDK8以前的HashMap实现方法)存放所有(以为这些字段是static)的ForkJoinTask任务的执行异常。
fork--安排任务异步执行
源码很简单,就不贴了,该方法其实就是将任务通过push方法加入到当前工作线程的工作队列或者提交队列(外部非ForkJoinWorkerThread线程通过submit、execute方法提交的任务),等待被线程池调度执行,这是一个非阻塞的立即返回方法。这里需要知道,ForkJoinPool线程池通过哈希数组+双端队列的方式将所有的工作线程拥有的任务队列和从外部提交的任务分别映射到哈希数组的不同槽位上,下一篇会介绍。将新任务始终push到队列一端的方式可以保证比较大的任务在队列的头部,越小的任务越在尾部,这时候拥有该任务队列的线程如果按照先进后出的方式pop弹出任务执行的话(这时候的任务队列就是当着栈来使用),将会是先从小任务开始,逐渐往大任务进行。而窃取任务的其他线程从对列头部开始窃取的话,将会帮助它完成大任务。
join---等待执行结果
//当计算完成时返回计算结果。此方法与get()的不同之处在于,异常完成会导致RuntimeException或Error,而不是ExecutionException,调用线程被中断不会通过抛出InterruptedException导致方法突然返回。
public final V join() {
int s;
if ((s = doJoin() & DONE_MASK) != NORMAL)
reportException(s); //非正常结束,抛出相关的异常堆栈信息
return getRawResult(); //正常结束,返回结果
} //等待任务执行结束并返回其状态status,该方法实现了join, get, quietlyJoin. 直接处理已经完成的,外部等待和unfork+exec的情况,其它情况转发到ForkJoinPool.awaitJoin
//如果 status < 0 则返回s;
//否则,若不是ForkJoinWorkerThread ,则等待 externalAwaitDone() 返回
//否则,若 (w = (wt = (ForkJoinWorkerThread)t).workQueue).tryUnpush(this) && (s = doExec()) < 0 则 返回s;
//否则,返回 wt.pool.awaitJoin(w, this, 0L)
private int doJoin() {
int s; Thread t; ForkJoinWorkerThread wt; ForkJoinPool.WorkQueue w;
return (s = status) < 0 ? s : //status为负数表示任务已经执行结束,直接返回status。
((t = Thread.currentThread()) instanceof ForkJoinWorkerThread) ?
(w = (wt = (ForkJoinWorkerThread)t).workQueue).
tryUnpush(this) && (s = doExec()) < 0 ? s : //调用pool的执行逻辑,并等待返回执行结果状态
wt.pool.awaitJoin(w, this, 0L) : //调用pool的等待机制
externalAwaitDone(); //不是ForkJoinWorkerThread,
} //抛出与给定状态关联的异常(如果有),被取消是CancellationException。
private void reportException(int s) {
if (s == CANCELLED)
throw new CancellationException();
if (s == EXCEPTIONAL)
rethrow(getThrowableException());
} public abstract V getRawResult(); //返回给定任务的执行异常(如果有的话),为了提供准确的异常堆栈信息,若异常不是由当前线程抛出的,将尝试以记录的异常为原因创建一个与抛出异常类型相同的新异常。
//如果没有那样的构造方法将尝试使用无参的构造函数,并通过设置initCause方法以达到同样的效果,尽管它可能包含误导的堆栈跟踪信息。
private Throwable getThrowableException() {
if ((status & DONE_MASK) != EXCEPTIONAL)
return null; //1. 通过当前任务对象的哈希值到哈希链表数组中找到相应的异常节点
int h = System.identityHashCode(this); //当前任务的hash值
ExceptionNode e;
final ReentrantLock lock = exceptionTableLock;
lock.lock(); //加锁
try {
expungeStaleExceptions(); //清理被GC回收的任务的异常节点
ExceptionNode[] t = exceptionTable;
e = t[h & (t.length - 1)]; //通过取模对应得索引获取哈希数组槽位中得节点
while (e != null && e.get() != this)
e = e.next; //遍历找到当前任务对应的异常节点
} finally {
lock.unlock();
}
Throwable ex;
if (e == null || (ex = e.ex) == null) //表示没有出现任何异常
return null;
if (e.thrower != Thread.currentThread().getId()) { //有异常但是不是由当前线程抛出的
Class<? extends Throwable> ec = ex.getClass();
try {
Constructor<?> noArgCtor = null;
Constructor<?>[] cs = ec.getConstructors();// public ctors only
//通过反射找到构造方法,并构造新异常
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) {
Throwable wx = (Throwable)c.newInstance(ex); //发现了我们期望的Throwable类型的参数的构造方法
return (wx == null) ? ex : wx;
}
}
if (noArgCtor != null) { //没有找到期望的构造方法,只能通过无参构造方法创建新异常
Throwable wx = (Throwable)(noArgCtor.newInstance());
if (wx != null) {
wx.initCause(ex); //将原始异常设置进去
return wx;
}
}
} catch (Exception ignore) {
}
}
return ex;
} //清除哈希链表数组中已经被GC回收掉的任务的异常节点。从exceptionTableRefQueue节点引用队列中获取异常节点并移除哈希链表数组中得对应节点
private static void expungeStaleExceptions() {
for (Object x; (x = exceptionTableRefQueue.poll()) != null;) {
if (x instanceof ExceptionNode) {
int hashCode = ((ExceptionNode)x).hashCode; //节点hash
ExceptionNode[] t = exceptionTable;
int i = hashCode & (t.length - 1); //取模得到哈希表索引
ExceptionNode e = t[i];
ExceptionNode pred = null;
while (e != null) {
ExceptionNode next = e.next;
if (e == x) { //找到了目标节点
if (pred == null)
t[i] = next;
else
pred.next = next;
break;
}
pred = e; //往后遍历链表
e = next;
}
}
}
} //窃取任务的主要执行方法,除非已经完成了,否则调用exec()并记录完成时的状态。
final int doExec() {
int s; boolean completed;
if ((s = status) >= 0) { //任务还未完成
try {
completed = exec(); 调用exec()并记录完成时的状态。
} catch (Throwable rex) {
return setExceptionalCompletion(rex); //记录异常并返回相关状态,并唤醒通过join等待此任务的线程。
}
if (completed)
s = setCompletion(NORMAL); //更新状态为正常结束,并唤醒通过join等待此任务的线程。
}
return s;
} //立即执行此任务的基本操作。返回true表示该任务已经正常完成,否则返回false表示此任务不一定完成(或不知道是否完成)。
//此方法还可能抛出(未捕获的)异常,以指示异常退出。此方法旨在支持扩展,一般不应以其他方式调用。
protected abstract boolean exec(); //等待未完成的非ForkJoinWorkerThread线程提交的任务执行结束,并返回任务状态status
private int externalAwaitDone() { //若是CountedCompleter任务,等待ForkJoinPool.common.externalHelpComplete((CountedCompleter<?>)this, 0) 返回
//否则,若ForkJoinPool.common.tryExternalUnpush(this),返回 doExec() 结果;
//否则,返回0
int s = ((this instanceof CountedCompleter) ? // try helping
ForkJoinPool.common.externalHelpComplete(
(CountedCompleter<?>)this, 0) : //辅助完成外部提交的CountedCompleter任务
ForkJoinPool.common.tryExternalUnpush(this) ? doExec() : 0); //辅助完成外部提交的非CountedCompleter任务
if (s >= 0 && (s = status) >= 0) { //表示任务还没结束,需要阻塞等待。
boolean interrupted = false;
do {
if (U.compareAndSwapInt(this, STATUS, s, s | SIGNAL)) { //标记有线程需要被唤醒
synchronized (this) {
if (status >= 0) {
try {
wait(0L); //任务还没结束,无限期阻塞直到被唤醒
} catch (InterruptedException ie) {
interrupted = true;
}
}
else
notifyAll(); //已经结束了唤醒所有阻塞的线程
}
}
} while ((s = status) >= 0);
if (interrupted)
Thread.currentThread().interrupt(); //恢复中断标识
}
return s;
} //记录异常,更新status状态,唤醒所有等待线程
private int setExceptionalCompletion(Throwable ex) {
int s = recordExceptionalCompletion(ex);
if ((s & DONE_MASK) == EXCEPTIONAL)
internalPropagateException(ex); //调用钩子函数传播异常
return s;
} /**
* 对任务异常结束的异常传播支持的钩子函数
*/
void internalPropagateException(Throwable ex) {
} //记录异常并设置状态status
final int recordExceptionalCompletion(Throwable ex) {
int s;
if ((s = status) >= 0) {
int h = System.identityHashCode(this); //哈希值
final ReentrantLock lock = exceptionTableLock;
lock.lock(); //加锁
try {
expungeStaleExceptions();
ExceptionNode[] t = exceptionTable;
int i = h & (t.length - 1);
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();
}
s = setCompletion(EXCEPTIONAL);
}
return s;
} //标记任务完成标志,并唤醒通过join等待此任务的线程。
private int setCompletion(int completion) {
for (int s;;) {
if ((s = status) < 0)
return s;
if (U.compareAndSwapInt(this, STATUS, s, s | completion)) { //更新状态
if ((s >>> 16) != 0)
synchronized (this) { notifyAll(); } //唤醒所有等待线程
return completion;
}
}
}
join方法就是ForkJoinTask最核心也最复杂的方法,就是等待任务执行结束并返回执行结果,若任务被取消抛出CancellationException异常,若是其他异常导致异常结束则抛出相关RuntimeException或Error信息,这些异常还可能包括由于内部资源耗尽而导致的RejectedExecutionException,比如分配内部任务队列失败。异常的处理利用了另一个哈希数组 + 链表的结构。该方法不会由于线程被中断而抛出InterruptedException异常,而是会在等到任务执行结束之后再将中断状态复位。
该方法的执行过程中调用了一些未实现的抽象方法:exec方法就是执行任务的入口,任务的逻辑与拆分策略都由该方法实现,只有返回true才表示任务正常完成。该方法可以抛出异常以指示异常结束。getRawResult方法用于返回任务正常结束的执行结果。internalPropagateException方法则是当任务异常的回调钩子函数。一般来讲,我们都会在exec方法里面实现如下的貌似递归的拆分逻辑(伪代码):
if 任务足够小 then
执行任务;
返回结果;
else
拆分成两个子任务t1、t2
t1.fork(); //提交到任务队列
t2.fork(); //提交到任务队列
Object result = t1.join() + t2.join(); //合并结果,这里的加号仅仅代表合并结果,并不是做加法运行
return result; //返回最终结果
我们知道,fork负责将任务推入队列,等待被调度执行,join则是等待执行任务,并返回结果,而join在执行任务的时候最终就是调用的exec,而exec中任务已经足够小就直接执行,否则会拆分任务之后通过fork将拆分出的子任务再次加入队列,其子任务执行的时候依然会执行exec(假设子任务的exec也是这样的实现),到时候又会继续拆分,或者足够小就直接执行,两个子任务合并结果之后是其父任务的结果,两个父任务的结果又合并成祖父任务的结果,以此类推就是递归的完成了整个任务。
get---获取异步任务结果
既然ForkJoinTask也是Future的子类,那么Future最重要的获取异步任务结果的get方法也必然要实现:
//如果需要,等待计算完成,然后检索其结果。
public final V get() throws InterruptedException, ExecutionException {
int s = (Thread.currentThread() instanceof ForkJoinWorkerThread) ? doJoin() : //是ForkJoinWorkerThread,执行doJoin
externalInterruptibleAwaitDone(); //执行externalInterruptibleAwaitDone
Throwable ex;
if ((s &= DONE_MASK) == CANCELLED)
throw new CancellationException(); //被取消的抛出CancellationException
if (s == EXCEPTIONAL && (ex = getThrowableException()) != null)
throw new ExecutionException(ex); //执行中出现异常的抛出相应的异常
return getRawResult(); //返回正常结果
} //阻塞非ForkJoinWorkerThread线程,直到完成或中断。
private int externalInterruptibleAwaitDone() throws InterruptedException {
int s;
if (Thread.interrupted())
throw new InterruptedException();
if ((s = status) >= 0 &&
(s = ((this instanceof CountedCompleter) ?
ForkJoinPool.common.externalHelpComplete(
(CountedCompleter<?>)this, 0) :
ForkJoinPool.common.tryExternalUnpush(this) ? doExec() :
0)) >= 0) { //根据不同的任务类型 返回执行或暂时等待被执行的状态
while ((s = status) >= 0) { //需要阻塞等待
if (U.compareAndSwapInt(this, STATUS, s, s | SIGNAL)) {
synchronized (this) {
if (status >= 0)
wait(0L); //阻塞等待
else
notifyAll(); //唤醒所有等待线程
}
}
}
}
return s;
}
get方法也是通过实现join方法的doJoin方法实现的,不同的是,调用get方法的线程如果被中断的话,get方法会立即抛出InterruptedException异常,而join方法则不会;另外任务异常完成的的相关异常,get方法会将相关异常都封装成ExecutionException异常,而join方法则是原样抛出相关的异常不会被封装成ExecutionException异常。get方法采用的wait/notifyAll这种线程通信机制来实现阻塞与唤醒。另外还有超时版本的get方法这里就不贴代码了,由此可见get支持可中断和/或定时等待完成。
invoke---立即执行任务,并等待返回结果
//开始执行此任务,如果需要等待其完成,并返回其结果,如果底层执行此任务时出现异常,则抛出相应的(未捕获的)RuntimeException或Error。
public final V invoke() {
int s;
if ((s = doInvoke() & DONE_MASK) != NORMAL)
reportException(s);
return getRawResult();
} // invoke, quietlyInvoke的实现
private int doInvoke() {
int s; Thread t; ForkJoinWorkerThread wt;
return (s = doExec()) < 0 ? s : //执行此任务,完成返回其status
((t = Thread.currentThread()) instanceof ForkJoinWorkerThread) ? //若未完成或需要等待就根据不同任务类型执行不同的等待逻辑
(wt = (ForkJoinWorkerThread)t).pool.
awaitJoin(wt.workQueue, this, 0L) :
externalAwaitDone();
}
invoke的实现会利用当前调用invoke的线程立即执行exec方法,当然如果exec方法的实现使用了fork/join,其还是会利用ForkJoinPool线程池的递归调度执行策略,等待子任务执行完成,一步步的合并成最终的任务结果,并返回。值得注意的是,该方法不会因为线程被中断而立即返回,而必须在等到任务执行有了结果之后才会对中断状态进行补偿。
invokeAll----批量执行任务,并等待它们执行结束
//执行两个任务
public static void invokeAll(ForkJoinTask<?> t1, ForkJoinTask<?> t2) {
int s1, s2;
t2.fork(); //t2任务交给线程池调度执行
if ((s1 = t1.doInvoke() & DONE_MASK) != NORMAL) //t1任务立即由当前线程执行
t1.reportException(s1); //若t1异常结束,则抛出异常,包括被取消的CancellationException
if ((s2 = t2.doJoin() & DONE_MASK) != NORMAL) //等待t2执行结束
t2.reportException(s2); //若t2异常结束,则抛出异常,包括被取消的CancellationException
} //执行任务数组
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) //都不能为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 = t.getException();
}
}
if (ex != null)
rethrow(ex); //若有任务异常结束,抛出数组最前面那个异常结束的任务的异常
} //批量执行任务,返回每个任务对应的ForkJoinTask实例,
public static <T extends ForkJoinTask<?>> Collection<T> invokeAll(Collection<T> tasks) {
if (!(tasks instanceof RandomAccess) || !(tasks instanceof List<?>)) {
invokeAll(tasks.toArray(new ForkJoinTask<?>[tasks.size()])); //将任务封装成ForkJoinTask,调用上面那个方法实现
return tasks;
}
//下面的逻辑与上面那个invokeAll也是一样的。
@SuppressWarnings("unchecked")
List<? extends ForkJoinTask<?>> ts = (List<? extends ForkJoinTask<?>>) tasks;
Throwable ex = null;
int last = ts.size() - 1;
for (int i = last; i >= 0; --i) {
ForkJoinTask<?> t = ts.get(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 = ts.get(i);
if (t != null) {
if (ex != null)
t.cancel(false);
else if (t.doJoin() < NORMAL)
ex = t.getException();
}
}
if (ex != null)
rethrow(ex);
return tasks;
}
批量任务的执行其实现都是排在前面的任务(只有两个参数是,第一个参数就是排在前面的任务,是数组或者队列时,索引越小的就是排在越前面的)由当前线程执行,后面的任务交给线程池调度执行,如果有多个任务都出现异常,只会抛出排在最前面那个任务的异常。
quietlyInvoke(),quietlyJoin()----不需要执行结果的invoke和join
源码就不贴了,quietlyInvoke(),quietlyJoin()这两个方法就仅仅了是调用了doInvoke和doJoin,然后就没有然后了,它们就是不关心执行结果版本的invoke和Join,当然异常结束的也不会将异常抛出来,当执行一组任务并且需要将结果或异常的处理延迟到全部任务完成时,这可能很有用。
cancel---尝试取消任务的执行
public boolean cancel(boolean mayInterruptIfRunning) {
return (setCompletion(CANCELLED) & DONE_MASK) == CANCELLED;
}
其主要通过setCompletion标记尚未完成的任务的状态为CANCELLED,并唤醒通过join等待此任务的线程。已经执行完成的任务无法被取消,返回true表示取消成功。注意该方法传入的mayInterruptIfRunning并没有使用,因此,ForkJoinTask不支持在取消任务时中断已经开始执行的任务,当然ForkJoinTask的子类可以重写实现。
tryUnfork---取消fork,即从任务队列中移除任务
//取消任务的执行计划。如果此任务是当前线程最近才刚刚通过fork安排执行,并且尚未在另一个线程中开始执行,则此方法通常会成功,但也不是100%保证会成功。
public boolean tryUnfork() {
Thread t;
return (((t = Thread.currentThread()) instanceof ForkJoinWorkerThread) ?
((ForkJoinWorkerThread)t).workQueue.tryUnpush(this) : //针对ForkJoinWorkerThread的取消逻辑
ForkJoinPool.common.tryExternalUnpush(this)); //针对外部提交任务的取消逻辑
}
tryUnfork尝试将该任务从任务队列中弹出,弹出之后线程池自然不会再调度该任务。该方法的实现只会在任务刚刚被推入任务队列,并且还处于任务队列的栈顶时才可能会成功,否则100%失败。
reinitialize---重新初始化该任务
public void reinitialize() {
if ((status & DONE_MASK) == EXCEPTIONAL) //有异常
clearExceptionalCompletion(); //从哈希链表数组中移除当前任务的异常节点,并将status重置为0
else
status = 0;
}
如果任务异常结束,会从异常哈希表中清除该任务的异常记录,该方法仅仅是将任务状态status重置为0,使得该任务可以被重新执行。
任务的完成状态查询----isDone、isCompletedNormally、isCancelled、isCompletedAbnormally
任务的执行状态可以在多个详细级别上查询:
- 如果任务以任何方式完成(包括任务在未执行的情况下被取消),则isDone为true。
- 如果任务在没有取消或没有遇到异常的情况下完成,则 isCompletedNormally 为true。
- 如果任务被取消(在这种情况下getException方法返回一个CancellationException),则 isCancelled 为true。
- 如果任务被取消或遇到异常,则isCompletedAbnormally异常为true,在这种情况下,getException将返回遇到的异常或java.util.concurrent.CancellationException。
为Runnable和Callable提供的adapt方法
adapt方法主要是为了兼容传统的Runnable和Callable任务,通过adapt方法可以将它们封装成ForkJoinTask任务,当将 ForkJoinTask 与其他类型的任务混合执行时,可以使用这些方法。
其他一些方法
getPool可以返回执行该任务的线程所在的线程池实例,inForkJonPool可以判定当前任务是否是由ForkJoinWorkerThread线程提交的,一般来说这意味着当前任务是内部拆分之后的子任务。
getQueuedTaskCount方法返回已经通过fork安排给当前工作线程执行,但还没有被执行的任务数量,该值是一个瞬间值。因为工作线程调度执行的任务通过fork提交的任务还是进入的该工作线程的任务队列,因此可以通过该任务得知该值。
其它一些方法:
//可能会在承载当前任务的执行池处于静默(空闲)状态时执行任务。这个方法可能在有很多任务都通过fork被安排执行,但是一个显示的join调用都没有,直到它们都被执行完的设计中使用。
//其实就是如果有一批任务被安排执行,并且不知道它们什么时候结束,如果希望在这些任务都执行结束之后再安排一个任务,就可以使用helpQuiesce。
public static void helpQuiesce() {
Thread t;
//根据执行线程的不同类型,调用不同的静默执行逻辑
if ((t = Thread.currentThread()) instanceof ForkJoinWorkerThread) {
ForkJoinWorkerThread wt = (ForkJoinWorkerThread)t;
wt.pool.helpQuiescePool(wt.workQueue);
}
else
ForkJoinPool.quiesceCommonPool();
} //返回被当前工作线程持有的任务数a比其它可能窃取其任务的其它工作线程持有的任务数b多多少的估计值,就是 a - b 的差值。若当前工作线程不是在ForkJoinPool中,则返回0
//通常该值被恒定在一个很小的值3,若超过这个阈值,则就在本地处理。
public static int getSurplusQueuedTaskCount() {
return ForkJoinPool.getSurplusQueuedTaskCount();
} //获取但不移除(即不取消执行计划)安排给当前线程的可能即将被执行的下一个任务。但不能保证该任务将在接下来实际被立即执行。该方法可能在即使任务存在但因为竞争而不可访问而返回null
//该方法主要是为了支持扩展,否则可能不会被使用。
protected static ForkJoinTask<?> peekNextLocalTask() {
Thread t; ForkJoinPool.WorkQueue q;
if ((t = Thread.currentThread()) instanceof ForkJoinWorkerThread)
q = ((ForkJoinWorkerThread)t).workQueue;
else
q = ForkJoinPool.commonSubmitterQueue();
return (q == null) ? null : q.peek();
} //获取并且移除(即取消执行)安排给当前线程的可能即将被执行的下一个任务。
//该方法主要是为了支持扩展,否则可能不会被使用。
protected static ForkJoinTask<?> pollNextLocalTask() {
Thread t;
return ((t = Thread.currentThread()) instanceof ForkJoinWorkerThread) ?
((ForkJoinWorkerThread)t).workQueue.nextLocalTask() :
null;
} //如果当前线程被ForkJoinPool运行,获取并且移除(即取消执行)当前线程即将可能执行的下一个任务。该任务可能是从其它线程中窃取来的。
//返回nulll并不一定意味着此任务正在操作的ForkJoinPool处于静止状态。该方法主要是为了支持扩展,否则可能不会被使用。
protected static ForkJoinTask<?> pollTask() {
Thread t; ForkJoinWorkerThread wt;
return ((t = Thread.currentThread()) instanceof ForkJoinWorkerThread) ?
(wt = (ForkJoinWorkerThread)t).pool.nextTaskFor(wt.workQueue) :
null;
}
一些说明
通常ForkJoinTask只适用于非循环依赖的纯函数的计算或孤立对象的操作,否则,执行可能会遇到某种形式的死锁,因为任务循环地等待彼此。但是,这个框架支持其他方法和技术(例如使用Phaser、helpQuiesce和complete),这些方法和技术可用于构造解决这种依赖任务的ForkJoinTask子类,为了支持这些用法,可以使用setForkJoinTaskTag或compareAndSetForkJoinTaskTag原子性地标记一个short类型的值,并使用getForkJoinTaskTag进行检查。ForkJoinTask实现没有将这些受保护的方法或标记用于任何目的,但是它们可以用于构造专门的子类,由此可以使用提供的方法来避免重新访问已经处理过的节点/任务。
ForkJoinTask应该执行相对较少的计算,并且应该避免不确定的循环。大任务应该被分解成更小的子任务,通常通过递归分解。如果任务太大,那么并行性就不能提高吞吐量。如果太小,那么内存和内部任务维护开销可能会超过处理开销。
ForkJoinTask是可序列化的,这使它们能够在诸如远程执行框架之类的扩展中使用。只在执行之前或之后序列化任务才是明智的,而不是在执行期间。
ForkJoinTask 的三个抽象子类
通常我们不会直接实现ForkJoinTask,而是实现其三个抽象子类,ForkJoinTask仅仅是为了配合ForkJoinPool实现任务的调度执行,通常我们使用的时候,仅仅只需要提供任务的拆分与执行即可,RecursiveAction 用于大多数不返回结果的计算, RecursiveTask 用于返回结果的计算, CountedCompleter 用于那些操作完成之后触发其他操作的操作。
RecursiveAction --- 不返回结果的任务
public abstract class RecursiveAction extends ForkJoinTask<Void> {
private static final long serialVersionUID = 5232453952276485070L; /**
* The main computation performed by this task.
*/
protected abstract void compute(); /**
* Always returns {@code null}.
*
* @return {@code null} always
*/
public final Void getRawResult() { return null; } /**
* Requires null completion value.
*/
protected final void setRawResult(Void mustBeNull) { } /**
* Implements execution conventions for RecursiveActions.
*/
protected final boolean exec() {
compute();
return true;
} }
RecursiveAction很简单,作为不返回结果的任务,其getRawResult方法永远返回null,setRawResult方法则什么都不做,它增加了一个无返回值的compute抽象方法,用于当ForkJoinTask被调度执行exec方法时调用,exec方法在执行完compute之后直接返回true,表示任务正常结束,而compute方法就是留给我们去实现大任务如何拆小任务,小任务怎么执行的逻辑。
RecursiveTask --- 要返回结果的任务
public abstract class RecursiveTask<V> extends ForkJoinTask<V> {
private static final long serialVersionUID = 5232453952276485270L; /**
* The result of the computation.
*/
V result; /**
* The main computation performed by this task.
* @return the result of the computation
*/
protected abstract V compute(); public final V getRawResult() {
return result;
} protected final void setRawResult(V value) {
result = value;
} /**
* Implements execution conventions for RecursiveTask.
*/
protected final boolean exec() {
result = compute();
return true;
} }
RecursiveTask也很简单,既然要返回结果,所以它定义了一个表示执行结果的result字段,getRawResult/setRawResult就用来操作该字段,它增加了一个有返回值的compute抽象方法,用于当ForkJoinTask被调度执行exec方法时调用,exec方法在执行完compute之后,将compute的返回结果作为任务的执行结果赋值给result,并最终返回true表示任务正常结束,同样compute方法也是留给我们去实现大任务如何拆小任务,小任务怎么执行,并且返回任务执行结果的逻辑。
CountedCompleter --- 操作完成触发钩子函数的操作
CountedCompleter是Java8才新增的一个ForkJoinTask的抽象子类,比前面两个要复杂的多(反正我目前是没怎么弄明白),它是为了在任务完成之后调用钩子函数,它可以支持返回结果(这时应该重写方法getRawResult()以提供join()、invoke()和相关方法的结果),也可以不返回结果,默认其getRawResult方法永远返回null,setRawResult方法也什么都不做。
它也增加了一个无返回值的compute抽象方法,用于当ForkJoinTask被调度执行exec方法时调用,但它的exec方法在正常执行完compute之后,永远都返回的是false,表示任务没有正常结束,根据ForkJoinTask的doExec方法实现,对于CountedCompleter正常结束但返回false这种实现,将导致不会执行setCompletion即改变任务的状态也不会唤醒等待该任务的线程,这都交给了CountedCompleter自己来完成,而compute若异常结束则还是按原来的逻辑记录异常到哈希链表数组中,然后改变任务的状态为EXCEPTIONAL,因此只有在显式调用complete(T)、ForkJoinTask.cancel(boolean)、ForkJoinTask.completeExceptionally(Throwable)或compute异常完成时,任务状态才会更改。
CountedCompleter在创建实例的时候还可以传入一个CountedCompleter实例,因此可以形成树状的任务结构,树上的所有任务是可以并行执行的,且每一个子任务完成后都可以通过tryComplete辅助其父任务的完成,CountedCompleter的代码量很少区区300行,但其设计理念却很有用,CountedCompleters在存在子任务停滞和阻塞的情况下比其他形式的ForkJoinTasks更健壮,但编程的直观性较差。CountedCompleter的使用类似于其他基于完成的组件(例如java.nio.channel.completionhandler),除了在完成时触发onCompletion(CountedCompleter)可能需要多个挂起任务的完成,而不仅仅是一个。除非进行了其他初始化,否则挂起计数从0开始,但是可以使用setPendingCount、addToPendingCount和compareAndSetPendingCount方法(原子性地)更改挂起计数。在调用tryComplete时,如果挂起的操作计数非零,则递减;否则,将触发onCompletion操作,如果该CountedCompleters本身还有一个CountedCompleters,则继续对其CountedCompleters进行该过程。
一个CountedCompleter实现类必须实现compute方法,在大多数情况下(如下所示),在其返回之前应该调用一次tryComplete()。该类还可以选择性地重写方法onCompletion(CountedCompleter),以便在正常完成时执行想要的操作,以及方法onExceptionalCompletion(Throwable, CountedCompleter),以便在任何异常时执行操作。
CountedCompleters通常不返回结果,在这种情况下,它们通常被声明为CountedCompleter<Void>,并且总是返回null作为结果值。在其他情况下,您应该重写方法getRawResult来提供来自join()、invoke()和相关方法的结果。通常,该方法应该返回CountedCompleter对象的一个字段(或一个或多个字段的函数)的值,该对象在完成时保存结果。方法setRawResult在CountedCompleters中默认什么也不做。重写此方法以维护包含结果数据的其他对象或字段是可能的,但很少适用。
一个没有其他CountedCompleter(例如getCompleter返回null)的CountedCompleter可以作为一个具有附加功能的常规的ForkJoinTask。然而,任何一个有其他CountedCompleter的CountedCompleter,那么它只能作为其他计算的内部辅助,因此它自身的任务状态(例如ForkJoinTask.isDone等方法报告的)将是无意义的,其状态只在显示的调用complete,ForkJoinTask.cancel, ForkJoinTask.completeExceptionally(Throwable) 或者compute方法抛出异常时才会被改变。在任何异常完成之后,如果存在一个并且还没有以其他方式完成的任务,则可能会将异常传播给任务的 CountedCompleter(以及它的CountedCompleter,以此类推)。类似地,取消一个内部CountedCompleter只会对该completer产生局部影响,因此通常不太有用。
CountedCompleters是一个抽象类,要理解它有点难度,其编程实现可以达到的目的也灵活多变,就Java doc提供了几个示例可以看出,CountedCompleters可以将任务递归分解成基于树的形状,基于树的技术通常也比直接fork叶子任务更可取,因为它们减少了线程间的通信并提高了负载平衡。它还可以用于搜索查找过程中,当一个线程搜索到目标就可以让根节点任务完成,其他子节点任务自发终止。还可以用于计算任务时子节点任务合并结果得父节点任务又与其兄弟节点合并结果得到祖父任务结果,以此类推这样的场景等等,总之CountedCompleter类的使用非常灵活,如java并行流中涉及到的运行集拆分,结果合并,运算调度等就有用到它。
Java并发包线程池之ForkJoinPool即ForkJoin框架(一)的更多相关文章
- Java并发包线程池之ForkJoinPool即ForkJoin框架(二)
前言 前面介绍了ForkJoinPool相关的两个类ForkJoinTask.ForkJoinWorkerThread,现在开始了解ForkJoinPool.ForkJoinPool也是实现了Exec ...
- java并发包&线程池原理分析&锁的深度化
java并发包&线程池原理分析&锁的深度化 并发包 同步容器类 Vector与ArrayList区别 1.ArrayList是最常用的List实现类,内部是通过数组实现的, ...
- Java并发包——线程池
Java并发包——线程池 摘要:本文主要学习了Java并发包中的线程池. 部分内容来自以下博客: https://www.cnblogs.com/dolphin0520/p/3932921.html ...
- Java并发包线程池之Executors、ExecutorCompletionService工具类
前言 前面介绍了Java并发包提供的三种线程池,它们用处各不相同,接下来介绍一些工具类,对这三种线程池的使用. Executors Executors是JDK1.5就开始存在是一个线程池工具类,它定义 ...
- Java并发包线程池之ScheduledThreadPoolExecutor
前言 它是一种可以安排在给定的延迟之后执行一次或周期性执行任务的ThreadPoolExecutor.因为它继承了ThreadPoolExecutor, 当然也具有处理普通Runnable.Calla ...
- Java并发包--线程池原理
转载请注明出处:http://www.cnblogs.com/skywang12345/p/3509954.html 线程池示例 在分析线程池之前,先看一个简单的线程池示例. 1 import jav ...
- Java并发包--线程池框架
转载请注明出处:http://www.cnblogs.com/skywang12345/p/3509903.html 线程池架构图 线程池的架构图如下: 1. Executor 它是"执行者 ...
- Java并发包线程池之ThreadPoolExecutor
参数详解 ExecutorService的最通用的线程池实现,ThreadPoolExecutor是一个支持通过配置一些参数达到满足不同使用场景的线程池实现,通常通过Executors的工厂方法进行配 ...
- 第9章 Java中的线程池 第10章 Exector框架
与新建线程池相比线程池的优点 线程池的分类 ThreadPoolExector参数.执行过程.存储方式 阻塞队列 拒绝策略 10.1 Exector框架简介 10.1.1 Executor框架的两级调 ...
随机推荐
- 【OF框架】新建库表及对应实体,并实现简单的增删改查操作,封装操作标准WebApi
准备 搭建好项目框架及数据库,了解框架规范. 1.数据库表和实体一一对应,表名实体名名字相同,用小写,下划线连接.字段名用驼峰命名法,首字母大写. 2.实体放在Entities目录下,继承Entity ...
- Windows——系统盘重置密码
一.制作好系统启动U盘 软碟通自己制作即可 二.这进入到安装前界面按Shift+F10调出命令提示符 三.输入regedit后按回车进入注册表编辑器 四. 左键单击选中HKEY_LOCAL_MACHI ...
- Arduino Tian开发板:一个功能强大的天气预报中心
每天都在出现新的连接设备. Arduino携手云平台一起加入这场战斗,于是出现了一个新的挑战者 - Arduino Tian! 使用python和经典Arduino框架,本教程将引导您将您的Ardui ...
- FM系列
在计算广告中,CTR是非常重要的一环.对于特征组合来说,业界通用的做法主要有两大类:FM系列和Tree系列.这里我们来介绍一下FM系列. 在传统的线性模型中,每个特征都是独立的,如果需要考虑特征与特征 ...
- Python读excel——xlrd
Python读excel——xlrd Python读取Excel表格,相比xlwt来说,xlrd提供的接口比较多,但过程也有几个比较麻烦的问题,比如读取日期.读合并单元格内容.下面先看看基本的操作: ...
- 【基础搜索】poj-2676-Sudoku(数独)--求补全九宫格的一种合理方案
数独 时限:2000 MS 内存限制:65536K 提交材料共计: 22682 接受: 10675 特别法官 描述 数独是一个非常简单的任务.一个9行9列的正方形表被分成9个较小的3x ...
- vmware 共享文件夹不显示文件的问题
上海SEO:安装vmtools后还是不显示执行以下操作//但是只有root权限才行 1:输入命令 sudo apt install open-vm-tools 安装工具2:输入命令 sudo vmh ...
- 洛谷P2221 高速公路【线段树】
题目:https://www.luogu.org/problemnew/show/P2221 题意:有n个节点排成一条链,相邻节点之间有一条路. C u v val表示从u到v的路径上的每条边权值都加 ...
- python - orm 字段
1.models.AutoField 自增列 = int(11) 如果没有的话,默认会生成一个名称为 id 的列,如果要显示的自定义一个自增列,必须将给列设置为主键 primary_key=True. ...
- 使用jQuery快速高效制作网页交互特效第一章JavaScript基础
JavaScript 一.JavaScript概念: JavaScript面向对象事件驱动具有安全性的脚本语言,面向对象 JavaScript特点: 1.解释性语言,边运行边解释 2.和HTML页面实 ...