ForkJoin框架之ForkJoinTask

 阅读约 62 分钟

前言

在前面的文章"CompletableFuture和响应式编程"中提到了ForkJoinTask和ForkJoinPool,后者毫无疑问是一个线程池,前者则是一个类似FutureTask经典定义的概念.

官方有一个非常无语的解释:ForkJoinTask就是运行在ForkJoinPool的一个任务抽象,ForkJoinPool就是运行ForkJoinTask的线程池.

ForkJoin框架包含ForkJoinTask,ForkJoinWorkerThread,ForkJoinPool和若干ForkJoinTask的子类,它的核心在于分治和工作窍取,最大程度利用线程池中的工作线程,避免忙的忙死,饿的饿死.

ForkJoinTask可以理解为类线程但比线程轻量的实体,在ForkJoinPool中运行的少量ForkJoinWorkerThread可以持有大量的ForkJoinTask和它的子任务.ForkJoinTask同时也是一个轻量的Future,使用时应避免较长阻塞和io.

ForkJoinTask在JAVA8中应用广泛,但它是一个抽象类,它的子类派生了各种用途,如后续计划单独介绍的CountedCompleter,以及若干JAVA8中stream api定义的与并行流有关的各种操作(ops).

源码

首先看ForkJoinTask的签名.

  1. public abstract class ForkJoinTask<V> implements Future<V>, Serializable

从签名上看,ForkJoinTask实现了future,也可以序列化,但它不是一个Runnable或Callable.

ForkJoinTask虽然可以序列化,但它只对运行前和后敏感,对于执行过程中不敏感.

先来看task的运行字段:

  1. //volatie修饰的任务状态值,由ForkJoinPool或工作线程修改.
  2. volatile int status;
  3. static final int DONE_MASK = 0xf0000000;//用于屏蔽完成状态位.
  4. static final int NORMAL = 0xf0000000;//表示正常完成,是负值.
  5. static final int CANCELLED = 0xc0000000;//表示被取消,负值,且小于NORMAL
  6. static final int EXCEPTIONAL = 0x80000000;//异常完成,负值,且小于CANCELLED
  7. static final int SIGNAL = 0x00010000;//用于signal,必须不小于1<<16,默认为1<<16.
  8. static final int SMASK = 0x0000ffff;//后十六位的task标签

很显然,DONE_MASK能够过滤掉所有非NORMAL,非CANCELLED,非EXCEPTIONAL的状态,字段的含义也很直白,后面的SIGNAL和SMASK还不明确,后面再看.

  1. //标记当前task的completion状态,同时根据情况唤醒等待该task的线程.
  2. private int setCompletion(int completion) {
  3. for (int s;;) {
  4. //开启一个循环,如果当前task的status已经是各种完成(小于0),则直接返回status,这个status可能是某一次循环前被其他线程完成.
  5. if ((s = status) < 0)
  6. return s;
  7. //尝试将原来的status设置为它与completion按位或的结果.
  8. if (U.compareAndSwapInt(this, STATUS, s, s | completion)) {
  9. if ((s >>> 16) != 0)
  10. //此处体现了SIGNAL的标记作用,很明显,只要task完成(包含取消或异常),或completion传入的值不小于1<<16,
  11. //就可以起到唤醒其他线程的作用.
  12. synchronized (this) { notifyAll(); }
  13. //cas成功,返回参数中的completion.
  14. return completion;
  15. }
  16. }
  17. }

前面用注释解释了这个方法的逻辑,显然该方法是阻塞的,如果传入的参数不能将status设置为负值会如何?

显然,可能会有至多一次的成功cas,并且若满足唤醒的条件,会尝试去唤醒线程,甚至可能因为为了唤醒其他线程而被阻塞在synchonized代码块外;也可能没有一次成功的cas,直到其他线程成功将status置为完成.

  1. //final修饰,运行ForkJoinTask的核心方法.
  2. final int doExec() {
  3. int s; boolean completed;
  4. //仅未完成的任务会运行,其他情况会忽略.
  5. if ((s = status) >= 0) {
  6. try {
  7. //调用exec
  8. completed = exec();
  9. } catch (Throwable rex) {
  10. //发生异常,用setExceptionalCompletion设置结果
  11. return setExceptionalCompletion(rex);
  12. }
  13. if (completed)
  14. //正常完成,调用前面说过的setCompletion,参数为normal,并将返回值作为结果s.
  15. s = setCompletion(NORMAL);
  16. }
  17. //返回s
  18. return s;
  19. }
  20. //记录异常并且在符合条件时传播异常行为
  21. private int setExceptionalCompletion(Throwable ex) {
  22. //首先记录异常信息到结果
  23. int s = recordExceptionalCompletion(ex);
  24. if ((s & DONE_MASK) == EXCEPTIONAL)
  25. //status去除非完成态标志位(只保留前4位),等于EXCEPTIONAL.内部传播异常
  26. internalPropagateException(ex);
  27. return s;
  28. }
  29. //internalPropagateException方法是一个空方法,留给子类实现,可用于completer之间的异常传递
  30. void internalPropagateException(Throwable ex) {}
  31. //记录异常完成
  32. final int recordExceptionalCompletion(Throwable ex) {
  33. int s;
  34. if ((s = status) >= 0) {
  35. //只能是异常态的status可以记录.
  36. //hash值禁止重写,不使用子类的hashcode函数.
  37. int h = System.identityHashCode(this);
  38. final ReentrantLock lock = exceptionTableLock;
  39. //异常锁,加锁
  40. lock.lock();
  41. try {
  42. //抹除脏异常,后面叙述
  43. expungeStaleExceptions();
  44. //异常表数组.ExceptionNode后面叙述.
  45. ExceptionNode[] t = exceptionTable;//exceptionTable是一个全局的静态常量,后面叙述
  46. //用hash值和数组长度进行与运算求一个初始的索引
  47. int i = h & (t.length - 1);
  48. for (ExceptionNode e = t[i]; ; e = e.next) {
  49. //找到空的索引位,就创建一个新的ExceptionNode,保存this,异常对象并退出循环
  50. if (e == null) {
  51. t[i] = new ExceptionNode(this, ex, t[i]);//(1)
  52. break;
  53. }
  54. if (e.get() == this) //已设置在相同的索引位置的链表中,退出循环.//2
  55. break;
  56. //否则e指向t[i]的next,进入下个循环,直到发现判断包装this这个ForkJoinTask的ExceptionNode已经出现在t[i]这个链表并break(2),
  57. //或者直到e是null,意味着t[i]出发开始的链表并无包装this的ExceptionNode,则将构建一个新的ExceptionNode并置换t[i],
  58. //将原t[i]置为它的next(1).整个遍历判断和置换过程处在锁中进行.
  59. }
  60. } finally {
  61. lock.unlock();
  62. }
  63. //记录成功,将当前task设置为异常完成.
  64. s = setCompletion(EXCEPTIONAL);
  65. }
  66. return s;
  67. }
  68. //exceptionTable声明
  69. private static final ExceptionNode[] exceptionTable;//全局异常node表
  70. private static final ReentrantLock exceptionTableLock;//上面用到的锁,就是一个普通的可重入锁.
  71. private static final ReferenceQueue<Object> exceptionTableRefQueue;//变量表引用队列,后面详述.
  72. private static final int EXCEPTION_MAP_CAPACITY = 32;//异常表的固定容量,不大,只有32而且是全局的.
  73. //初始化在一个静态代码块.
  74. static {
  75. exceptionTableLock = new ReentrantLock();
  76. exceptionTableRefQueue = new ReferenceQueue<Object>();
  77. exceptionTable = new ExceptionNode[EXCEPTION_MAP_CAPACITY];//容量
  78. try {
  79. U = sun.misc.Unsafe.getUnsafe();
  80. Class<?> k = ForkJoinTask.class;
  81. STATUS = U.objectFieldOffset
  82. (k.getDeclaredField("status"));
  83. } catch (Exception e) {
  84. throw new Error(e);
  85. }
  86. }
  87. //先来看ExceptionNode内部类的实现
  88. //签名,实现了一个ForkJoinTask的弱引用.
  89. static final class ExceptionNode extends WeakReference<ForkJoinTask<?>> {
  90. final Throwable ex;
  91. ExceptionNode next;
  92. final long thrower; // use id not ref to avoid weak cycles
  93. final int hashCode; // store task hashCode before weak ref disappears
  94. ExceptionNode(ForkJoinTask<?> task, Throwable ex, ExceptionNode next) {
  95. super(task, exceptionTableRefQueue);//指向弱引用的构造函数,保存引用为task,队列为全局的exceptionTableRefQueue.
  96. this.ex = ex;//抛出的异常的引用
  97. this.next = next;//数组中的ExceptionNode以链表形式存在,前面分析过,先入者为后入者的next
  98. this.thrower = Thread.currentThread().getId();//保存抛出异常的线程id(严格来说是创建了this的线程)
  99. this.hashCode = System.identityHashCode(task);//哈希码保存关联task的哈希值.
  100. }
  101. }
  102. //清除掉异常表中的脏数据,仅在持有全局锁时才可使用.前面看到在记录新的异常信息时要进行一次清除尝试
  103. private static void expungeStaleExceptions() {
  104. //循环条件,全局exceptionTableRefQueue队列不为空,前面说过ExceptionNode是弱引用,当它被回收时会被放入此队列.
  105. for (Object x; (x = exceptionTableRefQueue.poll()) != null;) {
  106. //从队首依次取出元素.
  107. if (x instanceof ExceptionNode) {
  108. //计算在全局exceptionTable中的索引.
  109. int hashCode = ((ExceptionNode)x).hashCode;
  110. ExceptionNode[] t = exceptionTable;
  111. int i = hashCode & (t.length - 1);
  112. //取出node
  113. ExceptionNode e = t[i];
  114. ExceptionNode pred = null;
  115. //不停遍历,直到e是null为止.
  116. while (e != null) {
  117. //e的next
  118. ExceptionNode next = e.next;//2
  119. //x是队首出队的元素.它与e相等说明找到
  120. if (e == x) {
  121. //e是一个链表的元素,pred表示它是否有前置元素
  122. if (pred == null)
  123. //无前置元素,说明e在链表首部,直接将首部元素指向next即可.
  124. t[i] = next;
  125. else
  126. //有前置元素,说明循环过若干次,将当前e出链表
  127. pred.next = next;
  128. //在链表中发现x即break掉内循环,继续从exceptionTableRefQueue的队首弹出新的元素.
  129. break;
  130. }
  131. //只要发现当前e不是x,准备下一次循环,pred指向e.e指向next,进行下一个元素的比较.
  132. pred = e;
  133. e = next;
  134. }
  135. }
  136. }
  137. }

到此doExec(也是每个ForkJoinTask的执行核心过程)就此结束.

很明显,ForkJoinTask的doExec负责了核心的执行,它留下了exec方法给子类实现,而重点负责了后面出现异常情况的处理.处理的逻辑前面已论述,在产生异常时尝试将异常存放在全局的execptionTable中,存放的结构为数组+链表,按哈希值指定索引,每次存放新的异常时,顺便清理上一次已被gc回收的ExceptionNode.所有ForkJoinTask共享了一个exceptionTable,因此必然在有关的几个环节要进行及时的清理.除了刚刚论述的过程,还有如下的几处:

前面论述了recordExceptionalCompletion,一共有四处使用了expungeStaleException,将已回收的ExceptionNode从引用队列中清除.

clearExceptionalCompletion在对一个ForkJoinTask重新初始化时使用,我们在前面提到序列化时说过,ForkJoinTask的序列化结果只保留了两种情况:运行前,运行结束.重新初始化一个ForkJoinTask,就要去除任何中间状态,包含自身产出的已被回收的异常node,而expungeStaleExceptions显然也顺便帮助其他task清除.

getThrowableException是查询task运行结果时调用,如一些get/join方法,很明显,记录这个异常的作用就在于返回给get/join,在这一块顺便清理已被回收的node,尤其是将自己运行时生成的node清除.

helpExpungeStaleExceptions是提供给ForkJoinPool在卸载worker时使用,顺便帮助清理全局异常表.

使用它们的方法稍后再论述,先来继续看ForkJoinTask的源码.

  1. //内部等待任务完成,直到完成或超时.
  2. final void internalWait(long timeout) {
  3. int s;
  4. //status小于0代表已完成,直接忽略wait.
  5. //未完成,则试着加上SIGNAL的标记,令完成任务的线程唤醒这个等待.
  6. if ((s = status) >= 0 &&
  7. U.compareAndSwapInt(this, STATUS, s, s | SIGNAL)) {
  8. //加锁,只有一个线程可以进入.
  9. synchronized (this) {
  10. //再次判断未完成.等待timeout,且忽略扰动异常.
  11. if (status >= 0)
  12. try { wait(timeout); } catch (InterruptedException ie) { }
  13. else
  14. //已完成则响醒其他等待者.
  15. notifyAll();
  16. }
  17. }
  18. }

internalWait方法逻辑很简单,首先判断是否未完成,满足未完成,则将标记位加上SIGNAL(可能已有别的线程做过),随后加锁double check status,还未完成则等待并释放锁,若发现已完成,或在后续被唤醒后发现已完成,则唤醒其他等待线程.通过notifyAll的方式避免了通知丢失.

同时,它的使用方法目前只有一个ForkJoinPool::awaitJoin,在该方法中使用循环的方式进行internalWait,满足了每次按截止时间或周期进行等待,同时也顺便解决了虚假唤醒.

继续看externalAwaitDone函数.它体现了ForkJoin框架的一个核心:外部帮助.

  1. //外部线程等待一个common池中的任务完成.
  2. private int externalAwaitDone() {
  3. int s = ((this instanceof CountedCompleter) ?
  4. //当前task是一个CountedCompleter,尝试使用common ForkJoinPool去外部帮助完成,并将完成状态返回.
  5. ForkJoinPool.common.externalHelpComplete(
  6. (CountedCompleter<?>)this, 0) :
  7. //当前task不是CountedCompleter,则调用common pool尝试外部弹出该任务并进行执行,
  8. //status赋值doExec函数的结果,若弹出失败(其他线程先行弹出)赋0.
  9. ForkJoinPool.common.tryExternalUnpush(this) ? doExec() : 0);
  10. if (s >= 0 && (s = status) >= 0) {
  11. //检查上一步的结果,即外部使用common池弹出并执行的结果(不是CountedCompleter的情况),或外部尝试帮助CountedCompleter完成的结果
  12. //status大于0表示尝试帮助完成失败.
  13. //扰动标识,初值false
  14. boolean interrupted = false;
  15. do {
  16. //循环尝试,先给status标记SIGNAL标识,便于后续唤醒操作.
  17. if (U.compareAndSwapInt(this, STATUS, s, s | SIGNAL)) {
  18. synchronized (this) {
  19. if (status >= 0) {
  20. try {
  21. //CAS成功,进同步块发现double check未完成,则等待.
  22. wait(0L);
  23. } catch (InterruptedException ie) {
  24. //若在等待过程中发生了扰动,不停止等待,标记扰动.
  25. interrupted = true;
  26. }
  27. }
  28. else
  29. //进同步块发现已完成,则唤醒所有等待线程.
  30. notifyAll();
  31. }
  32. }
  33. } while ((s = status) >= 0);//循环条件,task未完成.
  34. if (interrupted)
  35. //循环结束,若循环中间曾有扰动,则中断当前线程.
  36. Thread.currentThread().interrupt();
  37. }
  38. //返回status
  39. return s;
  40. }

externalAwaitDone的逻辑不复杂,在当前task为ForkJoinPool.common的情况下可以在外部进行等待和尝试帮助完成.方法会首先根据ForkJoinTask的类型进行尝试帮助,并返回当前的status,若发现未完成,则进入下面的等待唤醒逻辑.该方法的调用者为非worker线程.

相似的方法:externalInterruptibleAwaitDone

  1. private int externalInterruptibleAwaitDone() throws InterruptedException {
  2. int s;
  3. //不同于externalAwaitDone,入口处发现当前线程已中断,则立即抛出中断异常.
  4. if (Thread.interrupted())
  5. throw new InterruptedException();
  6. if ((s = status) >= 0 &&
  7. (s = ((this instanceof CountedCompleter) ?
  8. ForkJoinPool.common.externalHelpComplete(
  9. (CountedCompleter<?>)this, 0) :
  10. ForkJoinPool.common.tryExternalUnpush(this) ? doExec() :
  11. 0)) >= 0) {
  12. while ((s = status) >= 0) {
  13. if (U.compareAndSwapInt(this, STATUS, s, s | SIGNAL)) {
  14. synchronized (this) {
  15. if (status >= 0)
  16. //wait时也不catch中断异常,发生即抛出.
  17. wait(0L);
  18. else
  19. notifyAll();
  20. }
  21. }
  22. }
  23. }
  24. return s;
  25. }

externalInterruptibleAwaitDone的逻辑与externalAwaitDone相似,只是对中断异常的态度为抛,后者为catch.

它们的使用点,externalAwaitDone为doJoin或doInvoke方法调用,externalInterruptibleAwaitDone为get方法调用,很明显,join操作不可扰动,get则可以扰动.

下面来看看doJoin和doInvoke

  1. //join的核心方法
  2. private int doJoin() {
  3. int s; Thread t; ForkJoinWorkerThread wt; ForkJoinPool.WorkQueue w;
  4. //已完成,返回status,未完成再尝试后续
  5. return (s = status) < 0 ? s :
  6. //未完成,当前线程是ForkJoinWorkerThread,从该线程中取出workQueue,并尝试将
  7. //当前task出队然后执行,执行的结果是完成则返回状态,否则使用当线程池所在的ForkJoinPool的awaitJoin方法等待.
  8. ((t = Thread.currentThread()) instanceof ForkJoinWorkerThread) ?
  9. (w = (wt = (ForkJoinWorkerThread)t).workQueue).
  10. tryUnpush(this) && (s = doExec()) < 0 ? s :
  11. wt.pool.awaitJoin(w, this, 0L) :
  12. //当前线程不是ForkJoinWorkerThread,调用前面说的externalAwaitDone方法.
  13. externalAwaitDone();
  14. }
  15. //invoke的核心方法
  16. private int doInvoke() {
  17. int s; Thread t; ForkJoinWorkerThread wt;
  18. //先尝试本线程执行,不成功才走后续流程
  19. return (s = doExec()) < 0 ? s :
  20. ((t = Thread.currentThread()) instanceof ForkJoinWorkerThread) ?
  21. //与上一个方法基本相同,但在当前线程是ForkJoinWorkerThread时不尝试将该task移除栈并执行,而是等
  22. (wt = (ForkJoinWorkerThread)t).pool.
  23. awaitJoin(wt.workQueue, this, 0L) :
  24. externalAwaitDone();
  25. }

到此终于可以看一些公有对外方法了.有了前面的基础,再看get,join,invoke等方法非常简单.

  1. //get方法还有get(long time)的变种.
  2. public final V get() throws InterruptedException, ExecutionException {
  3. int s = (Thread.currentThread() instanceof ForkJoinWorkerThread) ?
  4. //当前线程是ForkJoinWorkerThread则调用前面提过的doJoin方法.
  5. //否则调用前述externalInterruptibleAwaitDone
  6. doJoin() : externalInterruptibleAwaitDone();
  7. Throwable ex;
  8. if ((s &= DONE_MASK) == CANCELLED)
  9. //异常处理,取消的任务,抛出CancellationException.
  10. throw new CancellationException();
  11. if (s == EXCEPTIONAL && (ex = getThrowableException()) != null)
  12. //异常处理,调用getThrowableException获取异常,封进ExecutionException.
  13. throw new ExecutionException(ex);
  14. //无异常处理,返回原始结果.
  15. return getRawResult();
  16. }
  17. //getRawResult默认为一个抽象实现,在ForkJoinTask中,并未保存该结果的字段.
  18. public abstract V getRawResult();
  19. //getThrowableException方法
  20. private Throwable getThrowableException() {
  21. //不是异常标识,直接返回null,从方法名的字面意思看,要返回一个可抛出的异常.
  22. if ((status & DONE_MASK) != EXCEPTIONAL)
  23. return null;
  24. //系统哈希码来定位ExceptionNode
  25. int h = System.identityHashCode(this);
  26. ExceptionNode e;
  27. final ReentrantLock lock = exceptionTableLock;
  28. //加异常表全局锁
  29. lock.lock();
  30. try {
  31. //先清理已被回收的异常node,前面已述.
  32. expungeStaleExceptions();
  33. ExceptionNode[] t = exceptionTable;
  34. e = t[h & (t.length - 1)];
  35. //循环找出this匹配的异常node
  36. while (e != null && e.get() != this)
  37. e = e.next;
  38. } finally {
  39. lock.unlock();
  40. }
  41. Throwable ex;
  42. //前面找不出异常node或异常node中存放的异常为null,则返回null
  43. if (e == null || (ex = e.ex) == null)
  44. return null;
  45. if (e.thrower != Thread.currentThread().getId()) {
  46. //不是当前线程抛出的异常.
  47. Class<? extends Throwable> ec = ex.getClass();
  48. try {
  49. Constructor<?> noArgCtor = null;//该异常的无参构造器
  50. Constructor<?>[] cs = ec.getConstructors();//该异常类公有构造器
  51. for (int i = 0; i < cs.length; ++i) {
  52. Constructor<?> c = cs[i];
  53. Class<?>[] ps = c.getParameterTypes();
  54. if (ps.length == 0)
  55. //构建器参数列表长度0说明存在无参构造器,存放.
  56. noArgCtor = c;
  57. else if (ps.length == 1 && ps[0] == Throwable.class) {
  58. //发现有参构造器且参数长度1且第一个参数类型是Throwable,说明可以存放cause.
  59. //反射将前面取出的ex作为参数,反射调用该构造器创建一个要抛出的Throwable.
  60. Throwable wx = (Throwable)c.newInstance(ex);
  61. //反射失败,异常会被catch,返回ex,否则返回wx.
  62. return (wx == null) ? ex : wx;
  63. }
  64. }
  65. if (noArgCtor != null) {
  66. //在尝试了寻找有参无参构造器,并发现只存在无参构造器的情况,用无参构造器初始化异常.
  67. Throwable wx = (Throwable)(noArgCtor.newInstance());
  68. if (wx != null) {
  69. //将ex设置为它的cause并返回它的实例.
  70. wx.initCause(ex);
  71. return wx;
  72. }
  73. }
  74. } catch (Exception ignore) {
  75. //此方法不可抛出异常,一定要成功返回.
  76. }
  77. }
  78. //有参无参均未成功,返回找到的异常.
  79. return ex;
  80. }
  81. //join公有方法
  82. public final V join() {
  83. int s;
  84. if ((s = doJoin() & DONE_MASK) != NORMAL)
  85. //调用doJoin方法阻塞等待的结果不是NORMAL,说明有异常或取消.报告异常.
  86. reportException(s);
  87. //等于NORMAL,正常执行完毕,返回原始结果.
  88. return getRawResult();
  89. }
  90. //报告异常,可在前一步判断执行status是否为异常态,然后获取并重抛异常.
  91. private void reportException(int s) {
  92. //参数s必须用DONE_MASK处理掉前4位以后的位.
  93. if (s == CANCELLED)
  94. //传入的状态码等于取消,抛出取消异常.
  95. throw new CancellationException();
  96. if (s == EXCEPTIONAL)
  97. //使用前面的getThrowableException方法获取异常并重新抛出.
  98. rethrow(getThrowableException());
  99. }
  100. //invoke公有方法.
  101. public final V invoke() {
  102. int s;
  103. //先尝试执行
  104. if ((s = doInvoke() & DONE_MASK) != NORMAL)
  105. //doInvoke方法的结果status只保留完成态位表示非NORMAL,则报告异常.
  106. reportException(s);
  107. //正常完成,返回原始结果.
  108. return getRawResult();
  109. }

终于,读到此处的读者将关键的方法线串了起来,前述的所有内部方法,常量和变量与公有接口的关系已经明了.

很显然,ForkJoinTask是个抽象类,且它并未保存任务的完成结果,也不负责这个结果的处理,但声明并约束了返回结果的抽象方法getRawResult供子类实现.

因此,ForkJoinTask的自身关注任务的完成/异常/未完成,子类关注这个结果的处理.

每当获取到任务的执行状态时,ForkJoinTask可根据status来判断是否是异常/正常完成,并进入相应的处理逻辑,最终使用子类实现的方法完成一个闭环.

如果理解为将ForkJoinTask和子类的有关代码合并起来,在结果/完成状态/异常信息这一块,相当于同时有三个part在合作.

第一个part:status字段,它同时表示了未完成/正常完成/取消/异常完成等状态,也同时告诉有关等待线程是否要唤醒其他线程(每个线程等待前会设置SIGNAL),同时留出了后面16位对付其他情况.

第二个part:result,在ForkJoinTask见不到它,也没有相应的字段,子类也未必需要提供这个result字段,前面提到的CountedCompleter就没有提供这个result,它的getRawResult会固定返回null.但是CountedCompleter可以继承子类并实现这个result的保存与返回(道格大神在注释中举出了若干典型代码例子),在JAVA8中,stream api中的并行流也会保存每一步的计算结果,并对结果进行合并.

第三个part:异常.在ForkJoinTask中已经完成了所有异常处理流程和执行流程的定义,重点在于异常的存放,它是由ForkJoinTask的类变量进行存放的,结构为数组+链表,且元素利用了弱引用,借gc帮助清除掉已经被回收的ExceptionNode,显然在gc之前必须得到使用.而异常随时可以发生并进行record入列,但相应的能消费掉这个异常的只有相应的外部的get,join,invoke等方法或者内部扩展了exec()等方式,得到其他线程执行的task异常结果的情况.巧妙的是,只有外部调用者调用(get,invoke,join)时,这个异常信息才足够重要,需要rethrow出去并保存关键的堆栈信息;而内部线程在访问一些非自身执行的任务时,往往只需要status判断是否异常即可,在exec()中fork新任务的,也往往必须立即join这些新的子任务,这就保证了能够及时得到子任务中的异常堆栈(即使拿不到堆栈也知道它失败了).

经过前面的论述,ForkJoinTask的执行和异常处理已经基本论结,但是,一个ForkJoinTask在创建之后是如何运行的?显然,它不是一个Runnable,也不是Callable,不能直接submit或execute到普通的线程池.

临时切换到ForkJoinPool的代码,前面提到过,ForkJoinTask的官方定义就是可以运行在ForkJoinPool中的task.

  1. //ForkJoinPool代码,submit一个ForkJoinTask到ForkJoinPool,并将该task自身返回.
  2. //拿到返回的task,我们就可以进行前述的get方法了.
  3. public <T> ForkJoinTask<T> submit(ForkJoinTask<T> task) {
  4. if (task == null)
  5. throw new NullPointerException();
  6. externalPush(task);
  7. return task;
  8. }
  9. //execute,不返回.类似普通线程池提交一个runnable的行为.
  10. public void execute(ForkJoinTask<?> task) {
  11. if (task == null)
  12. throw new NullPointerException();
  13. externalPush(task);
  14. }

显然,若要使用一个自建的ForkJoinPool,可以使用execute或submit函数提交入池,然后用前述的get方法和变种方法进行.这是一种运行task的方式.

前面论述过的invoke方法会先去先去尝试本地执行,然后才去等待,故我们自己new一个ForkJoinTask,一样可以通过invoke直接执行,这是第二种运行task的方式.

前面论述的join方法在某种情况下也是一种task的运行方式,在当前线程是ForkJoinWorkerThread时,会去尝试将task出队并doExec,也就是会先用本线程执行一次,不成功才干等,非ForkJoinWorkerThread则直接干等了.显然我们可以自己构建一个ForkJoinWorkerThread并去join,这时会将任务出队并执行(但存在一个问题:什么时候入队).且出队后若未执行成功,则awaitJoin(参考ForkJoinPool::awaitJoin),此时因任务已出队,不会被窃取或帮助(在awaitJoin中会有helpStealer,但其实任务是当前线程自己"偷走"了),似乎完全要靠自己了.但并不表示ForkJoinTask子类无法获取这个已出队的任务,比如CountedCompleter使用时,可以在compute中新生成的Completer时,将源CountedCompleter(ForkJoinTask的子类)作为新生成的CountedCountedCompleter的completer(该子类中的一个字段),这样,若有一个ForkJoinWorkerThread窃取了这个新生成的CountedCompleter,可以通过completer链表找到先前被出队的CountedCompleter(ForkJoinTask).关于CountedCompleter单独文章详述.

除此之外呢?包含前面提到的使用join操作不是ForkJoinWorkerThread调用的情况,不使用ForkJoinPool的submit execute入池,如何能让一个ForkJoinTask在将来执行?我们来看后面的方法.

  1. //fork方法,将当前任务入池.
  2. public final ForkJoinTask<V> fork() {
  3. Thread t;
  4. if ((t = Thread.currentThread()) instanceof ForkJoinWorkerThread)
  5. //如果当前线程是ForkJoinWorkerThread,将任务压入该线程的任务队列.
  6. ((ForkJoinWorkerThread)t).workQueue.push(this);
  7. else
  8. //否则调用common池的externalPush方法入队.
  9. ForkJoinPool.common.externalPush(this);
  10. return this;
  11. }

显然,我们还可以通过对一个ForkJoinTask进行fork方法入池,入哪个池完全取决于当前线程的类型.这是第四种让任务能被运行的方式.

同样,我们也看到了第五种方式,ForkJoinPool.common其实就是一个常量保存的ForkJoinPool,它能够调用externalPush,我们自然也可以直接new一个ForkJoinPool,然后将当前task进行externalPush,字面意思外部压入.这种办法,非ForkJoinWorkerThread也能将任务提交到非common的ForkJoinPool.

从名字来看,ForkJoinTask似乎已经说明了一切,按照官方的注释也是如此.对一个task,先Fork压队,再Join等待执行结果,这是一个ForkJoinTask的执行周期闭环(但不要简单理解为生命周期,前面提到过,任务可以被重新初始化,而且重新初始化时还会清空ExceptionNode数组上的已回收成员).

到此为止,ForkJoinTask的核心函数和api已经基本了然,其它同类型的方法以及周边的方法均不难理解,如invokeAll的各种变种.下面来看一些"周边"类型的函数.有前述的基础,它们很好理解.

  1. //取消一个任务的执行,直接将status设置成CANCELLED,设置后判断该status 是否为CANCELLED,是则true否则false.
  2. public boolean cancel(boolean mayInterruptIfRunning) {
  3. return (setCompletion(CANCELLED) & DONE_MASK) == CANCELLED;
  4. }
  5. //判断是否完成,status小于0代表正常完成/异常完成/取消,很好理解.
  6. public final boolean isDone() {
  7. return status < 0;
  8. }
  9. //判断当前任务是否取消.
  10. public final boolean isCancelled() {
  11. //status前4位
  12. return (status & DONE_MASK) == CANCELLED;
  13. }
  14. public final boolean isCompletedAbnormally() {
  15. //是否为异常完成,前面说过,CANCELLED和EXCEPTIONAL均小于NORMAL
  16. return status < NORMAL;
  17. }
  18. //是否正常完成.
  19. public final boolean isCompletedNormally() {
  20. //完成态位等于NORMAL
  21. return (status & DONE_MASK) == NORMAL;
  22. }
  23. //获取异常.
  24. public final Throwable getException() {
  25. int s = status & DONE_MASK;
  26. //当为正常完成或未完成时,返回null.
  27. return ((s >= NORMAL) ? null :
  28. //是取消时,新建一个取消异常.
  29. (s == CANCELLED) ? new CancellationException() :
  30. //不是取消,参考前面提到的getThrowableException.
  31. getThrowableException());
  32. }
  33. //使用异常完成任务.
  34. public void completeExceptionally(Throwable ex) {
  35. //参考前述的setExceptionalCompletion,
  36. //ex已经是运行时异常或者Error,直接使用ex完成,若是受检异常,包装成运行时异常.
  37. setExceptionalCompletion((ex instanceof RuntimeException) ||
  38. (ex instanceof Error) ? ex :
  39. new RuntimeException(ex));
  40. }
  41. //使用value完成任务.
  42. public void complete(V value) {
  43. try {
  44. //设置原始结果,它是一个空方法.前面说过ForkJoinTask没有维护result之类的结果字段,子类可自行发挥.
  45. setRawResult(value);
  46. } catch (Throwable rex) {
  47. //前述步骤出现异常,就用异常方式完成.
  48. setExceptionalCompletion(rex);
  49. return;
  50. }
  51. //前面的结果执行完,标记当前为完成.
  52. setCompletion(NORMAL);
  53. }
  54. //安静完成任务.直接用NORMAL setCompletion,没什么好说的.
  55. public final void quietlyComplete() {
  56. setCompletion(NORMAL);
  57. }
  58. //安静join,它不会返回result也不会抛出异常.处理集合任务时,如果需要所有任务都被执行而不是一个执行出错(取消)其他也跟着出错的情况下,
  59. //很明显适用,这不同于invokeAll,静态方法invokeAll或invoke(ForkJoinTask,ForkJoinTask)会在任何一个任务出现异常后取消执行并抛出.
  60. public final void quietlyJoin() {
  61. doJoin();
  62. }
  63. //安静执行一次,不返回结果不抛出异常,没什么好说的.
  64. public final void quietlyInvoke() {
  65. doInvoke();
  66. }
  67. //重新初台化当前task
  68. public void reinitialize() {
  69. if ((status & DONE_MASK) == EXCEPTIONAL)
  70. //如果当前任务是异常完成的,清除异常.该方法参考前面的论述.
  71. clearExceptionalCompletion();
  72. else
  73. //否则重置status为0.
  74. status = 0;
  75. }
  76. //反fork.
  77. public boolean tryUnfork() {
  78. Thread t;
  79. //当前线程是ForkJoinWorkerThread,从它的队列尝试移除.
  80. return (((t = Thread.currentThread()) instanceof ForkJoinWorkerThread) ?
  81. ((ForkJoinWorkerThread)t).workQueue.tryUnpush(this) :
  82. //当前线程不是ForkJoinWorkerThread,用common池外部移除.
  83. ForkJoinPool.common.tryExternalUnpush(this));
  84. }

上面是一些简单的周边方法,大多并不需要再论述了,unfork方法很明显在某些场景下不会成功,显然,当一个任务刚刚入队并未进行后续操作时,很可能成功.按前面所述,当对一个任务进行join时,可能会成功的弹出当前任务并执行,此时不可能再次弹出;当一个任务被其他线程窃取或被它本身执行的也不会弹出.

再来看一些老朋友,在前面的文章"CompletableFuture和响应式编程"一文中,作者曾着重强调过它将每个要执行的动作进行压栈(未能立即执行的情况),而栈中的元素Completion即是ForkJoinTask的子类,而标记该Completion是否被claim的方法和周边方法如下:

  1. //获取ForkJoinTask的标记,返回结果为short型
  2. public final short getForkJoinTaskTag() {
  3. //status的后16位
  4. return (short)status;
  5. }
  6. //原子设置任务的标记位.
  7. public final short setForkJoinTaskTag(short tag) {
  8. for (int s;;) {
  9. //不停循环地尝试将status的后16位设置为tag.
  10. if (U.compareAndSwapInt(this, STATUS, s = status,
  11. //替换的结果,前16位为原status的前16位,后16位为tag.
  12. (s & ~SMASK) | (tag & SMASK)))
  13. //返回被换掉的status的后16位.
  14. return (short)s;
  15. }
  16. }
  17. //循环尝试原子设置标记位为tag,前提是原来的标记位等于e,成功true失败false
  18. public final boolean compareAndSetForkJoinTaskTag(short e, short tag) {
  19. for (int s;;) {
  20. if ((short)(s = status) != e)
  21. //如果某一次循环的原标记位不是e,则返回false.
  22. return false;
  23. //同上个方法
  24. if (U.compareAndSwapInt(this, STATUS, s,
  25. (s & ~SMASK) | (tag & SMASK)))
  26. return true;
  27. }
  28. }

还记得CompletableFuture在异步执行Completion时要先claim吗?claim方法中,会尝试设置这个标记位.这是截止jdk8中CompletableFuture使用到ForkJoinTask的功能.

目前来看,在CompletableFuture的内部实现Completion还没有使用到ForkJoinTask的其他属性,比如放入一个ForkJoinPool执行(没有任何前面总结的调用,比如用ForkJoinPool的push,execute,submit等,也没有fork到common池).但是很明显,道格大神令它继承自ForkJoinTask不可能纯粹只为了使用区区一个标记位,试想一下,在如此友好支持响应式编程的CompletableFuture中传入的每一个action都可以生成若干新的action,那么CompletableFuture负责将这些action封装成Completion放入ForkJoinPool执行,将最大化利用到ForkJoin框架的工作窃取和外部帮助的功效,强力结合分治思想,这将是多么优雅的设计.或者在jdk9-12中已经出现了相应的Completion实现(尽管作者写过JAVA9-12,遗憾的是也没有去翻它们的源码).

另外,尽管Completion的众多子类也没有result之类的表示结果的字段,但它的一些子类通过封装,实际上间接地将这个Completion所引用的dep的result作为了自己的"result",当然,getRawResult依旧是null,但是理念却是相通的.

以上是ForkJoinTask的部分核心源码,除了上述的源码外,还有一些同属于ForkJoinTask的核心源码部分,比如其他的public方法(参考join fork invoke 即可),一些利用ForkJoinPool的实现,要深入了解ForkJoinPool才能了解的方法,一些不太难的静态方法等,这些没有必要论述了.

除了核心源码外,ForkJoinTask也提供了对Runnable,Callable的适配器实现,这块很好理解,简单看一看.

  1. //对Runnable的实现,如果在ForkJoinPool中提交一个runnable,会用它封装成ForkJoinTask
  2. static final class AdaptedRunnable<T> extends ForkJoinTask<T>
  3. implements RunnableFuture<T> {
  4. final Runnable runnable;
  5. T result;
  6. AdaptedRunnable(Runnable runnable, T result) {
  7. //不能没有runnable
  8. if (runnable == null) throw new NullPointerException();
  9. this.runnable = runnable;
  10. //对runnable做适配器时,可以提交将结果传入,并设置为当前ForkJoinTask子类的result.
  11. //前面说过,ForkJoinTask不以result作为完成标记,判断一个任务是否完成或异常,使用status足以,
  12. //返回的结果才使用result.
  13. this.result = result;
  14. }
  15. public final T getRawResult() { return result; }
  16. public final void setRawResult(T v) { result = v; }
  17. //前面说过提交入池的ForkJoinTask最终会运行doExec,而它会调用exec,此处会调用run.
  18. public final boolean exec() { runnable.run(); return true; }
  19. public final void run() { invoke(); }
  20. private static final long serialVersionUID = 5232453952276885070L;//序列化用
  21. }
  22. //无结果的runnable适配器
  23. static final class AdaptedRunnableAction extends ForkJoinTask<Void>
  24. implements RunnableFuture<Void> {
  25. final Runnable runnable;
  26. AdaptedRunnableAction(Runnable runnable) {
  27. if (runnable == null) throw new NullPointerException();
  28. this.runnable = runnable;
  29. }
  30. //区别就是result固定为null,也不能set
  31. public final Void getRawResult() { return null; }
  32. public final void setRawResult(Void v) { }
  33. public final boolean exec() { runnable.run(); return true; }
  34. public final void run() { invoke(); }
  35. private static final long serialVersionUID = 5232453952276885070L;
  36. }
  37. //对runnable的适配器,但强制池中的工作线程在执行任务发现异常时抛出
  38. static final class RunnableExecuteAction extends ForkJoinTask<Void> {
  39. final Runnable runnable;
  40. RunnableExecuteAction(Runnable runnable) {
  41. if (runnable == null) throw new NullPointerException();
  42. this.runnable = runnable;
  43. }
  44. //默认null结果,set也是空实现
  45. public final Void getRawResult() { return null; }
  46. public final void setRawResult(Void v) { }
  47. public final boolean exec() { runnable.run(); return true; }
  48. void internalPropagateException(Throwable ex) {
  49. //前面说过doExec会被执行,它会调exec并catch,在catch块中设置当前任务为异常完成态,
  50. //然后调用internalPropagateException方法,而在ForkJoinTask中默认为空实现.
  51. //此处将异常重新抛出,将造成worker线程抛出异常.
  52. rethrow(ex);
  53. }
  54. private static final long serialVersionUID = 5232453952276885070L;
  55. }
  56. //对callable的适配器,当将callable提交至ForkJoinPool时使用.
  57. static final class AdaptedCallable<T> extends ForkJoinTask<T>
  58. implements RunnableFuture<T> {
  59. final Callable<? extends T> callable;
  60. T result;
  61. AdaptedCallable(Callable<? extends T> callable) {
  62. if (callable == null) throw new NullPointerException();
  63. this.callable = callable;
  64. }
  65. //字段中有一个result,直接使用它返回.
  66. public final T getRawResult() { return result; }
  67. //result可外部直接设置.
  68. public final void setRawResult(T v) { result = v; }
  69. public final boolean exec() {
  70. try {
  71. //默认的result用call函数设置.
  72. result = callable.call();
  73. return true;
  74. } catch (Error err) {
  75. //catch住Error,抛出
  76. throw err;
  77. } catch (RuntimeException rex) {
  78. //catch住运行时异常,抛出
  79. throw rex;
  80. } catch (Exception ex) {
  81. //catch住受检异常,包装成运行时异常抛出.
  82. throw new RuntimeException(ex);
  83. }
  84. }
  85. //run方法一样只是调用invoke,进而调用doExec.
  86. public final void run() { invoke(); }
  87. private static final long serialVersionUID = 2838392045355241008L;
  88. }
  89. //runnable生成适配器的工具方法
  90. public static ForkJoinTask<?> adapt(Runnable runnable) {
  91. return new AdaptedRunnableAction(runnable);
  92. }
  93. //指定结果设置runnable的适配器工具方法
  94. public static <T> ForkJoinTask<T> adapt(Runnable runnable, T result) {
  95. return new AdaptedRunnable<T>(runnable, result);
  96. }
  97. //对callable生成适配器的方法.
  98. public static <T> ForkJoinTask<T> adapt(Callable<? extends T> callable) {
  99. return new AdaptedCallable<T>(callable);
  100. }

以上的代码都不复杂,只要熟悉了ForkJoinTask的本身代码结构,对于这一块了解非常容易,这也间接说明了ForkJoinPool中是如何处理Runnable和Callable的(因为ForkJoinPool本身也是一种线程池,可以接受提交Callable和Runnable).

将runnable提交到pool时,可以指定result,也可以不指定,也可以用submit或execute方法区分异常处理行为,ForkJoinPool会自行选择相应的适配器.

将callable 提交到pool时,pool会选择对callable的适配器,它的结果将为task的结果,它的异常将为task的异常.

到此为止,ForkJoinTask的源码分析完成.

后语

本文详细分析了ForkJoinTask的源码,并解释了前文CompletableFuture中Completion与它的关联,以及分析了Completion继承自ForkJoinTask目前已带来的功能利用(tag)和将来可能增加的功用(一个Completion产生若干多个Completion并在ForkJoinPool中运行,还支持工作窃取).

同时本文也对ForkJoinPool和ForkJoinWorkerThread,以及CountedCompleter和Stream api中的并行流进行了略微的描述.

在文章的最后,或许有一些新手读者会好奇,我们究竟什么时候会使用ForkJoinTask?

首先,如果你在项目中大肆使用了流式计算,并使用了并行流,那么你已经在使用了.

前面提过,官方解释ForkJoinTask可以视作比线程轻量许多的实体,也是轻量的Future.结合在源码中时不时出来秀存在感的ForkJoinWorkerThread,显然它就是据说比普通线程轻量一些的线程,在前面的源码中可以看出,它维护了一组任务的队列,每个线程负责完成队列中的任务,也可以偷其他线程的任务,甚至池外的线程都可以时不时地来个join,顺便帮助出队执行任务.

显然,对于重计算,轻io,轻阻塞的任务,适合使用ForkJoinPool,也就使用了ForkJoinTask,你不会认为它可以提交runnable和callable,就可以不用ForkJoinTask了吧?前面的适配器ForkJoinPool在这种情况下必用的,可以去翻相应的源码.

本章没有去详述CountedCompleter,但前面论述时说过,你可以在exec()中将一个计算复杂的任务拆解为小的子任务,然后将子任务入池执行,父任务合并子任务的结果.这种分治的算法此前基本是在单线程模式下运行,使用ForkJoinTask,则可以将这种计算交给一个ForkJoinPool中的所有线程并行执行.

转自: https://segmentfault.com/a/1190000019549838?utm_source=tag-newest

ForkJoinPool源码简单解析的更多相关文章

  1. node-pre-gyp以及node-gyp的源码简单解析(以安装sqlite3为例)

    title: node-pre-gyp以及node-gyp的源码简单解析(以安装sqlite3为例) date: 2020-11-27 tags: node native sqlite3 前言 简单来 ...

  2. springmvc(2)Controller源码简单解析

    前面简单的分析了一下DispatcherServlet,接下来分析一下Controller,在web的MVC中,Controller就是其中的C,启动的一些页面逻辑处理,页面映射的功能: 首先看看超类 ...

  3. Okhttp源码简单解析(一)

    业余时间把源码clone下来大致溜了一遍,并且也参阅了其余大神的博客,在这里把自己的心得记录下来共享之,如有不当的地方欢迎批评指正.本文是Okttp源码解析系列的第一篇,不会深入写太多的东西,本篇只是 ...

  4. StringBudiler源码简单解析

    StringBudiler源码 继承关系树 底层实现 默认容量() 特别的添加方法(append) 1.继承关系树 继承自AbstractStringBuilder与StringBuffer同族 2. ...

  5. springmvc(1)DispatcherServlet源码简单解析

    springmvc的简单配置 1.首先需要在web.xml中配置DispatcherServlet,这个类是springmvc的核心类,所以的操作都是由这里开始,并且大部分都是在这里面实现的,比如各种 ...

  6. FFmpeg的HEVC解码器源码简单分析:解析器(Parser)部分

    ===================================================== HEVC源码分析文章列表: [解码 -libavcodec HEVC 解码器] FFmpeg ...

  7. iOS开发之Masonry框架源码深度解析

    Masonry是iOS在控件布局中经常使用的一个轻量级框架,Masonry让NSLayoutConstraint使用起来更为简洁.Masonry简化了NSLayoutConstraint的使用方式,让 ...

  8. Retrofit源码设计模式解析(下)

    本文将接着<Retrofit源码设计模式解析(上)>,继续分享以下设计模式在Retrofit中的应用: 适配器模式 策略模式 观察者模式 单例模式 原型模式 享元模式 一.适配器模式 在上 ...

  9. Android 图片加载框架Glide4.0源码完全解析(二)

    写在之前 上一篇博文写的是Android 图片加载框架Glide4.0源码完全解析(一),主要分析了Glide4.0源码中的with方法和load方法,原本打算是一起发布的,但是由于into方法复杂性 ...

随机推荐

  1. RabbitMQ绑定、队列、消息、虚拟主机详解(五)

    Binding:绑定,Exchange和Exchange.Queue之间的连接关系 Binding中可以包含RoutingKey或者参数 Queue:消息队列,实际存储消息数据 Durability: ...

  2. 170815-关于Filter的知识点

    Filter简介:         Filter翻译为中文是过滤器的意思.         Filter是JavaWeb的三大web组件之一:Servlet.Filter.Listener       ...

  3. Spring Boot 集成 JPA 的步骤

    Spring Boot 集成 JPA 的步骤 配置依赖 compile group: 'org.springframework.boot', name: 'spring-boot-starter-da ...

  4. LuceneNET全文检索封装

    一.源码特点       1.  Lucene.net是Lucene的.net移植版本,是一个开源的全文检索引擎开发包,即它不是一个完整的全文检索引擎,而是一个全文检索引擎的架构,提供了完整的查询引擎 ...

  5. linux gsensor驱动分析【转】

    本文转载自:http://blog.sina.com.cn/s/blog_89f592f501013sr2.html 本文以Bma250驱动为例子,详细介绍Gsensor设计的一个模板. gsenso ...

  6. optistruct非线性分析步子步设置

    The CNTNLSUB command can be used in the Subcase Information section to continue a nonlinear solution ...

  7. Oracle-buffer cache、shared pool

    http://blog.csdn.net/panfelix/article/details/38347059   buffer pool 和shared pool 详解 http://blog.csd ...

  8. day52—JavaScript拖拽事件的应用(自定义滚动条)

    转行学开发,代码100天——2018-05-07 前面的记录里展示了JavaScript中鼠标拖拽功能,今天利用拖拽功能实现另一个应用场景——自定义滚动条(作为控制器)的用法. 常通过自定义滚动条控制 ...

  9. shell命令传参数(参数长度不定)

    脚本 sudo echo "[mysqlMaster<$1>]" >> /home/admin/hostrecord count= ];do >> ...

  10. Kotlin学习(2)函数

    函数声明: fun plus(a:Int,b:String):Boolean{ //fun 函数名(参数名:参数类型,参数名:参数类型):返回值类型 println(a) return true // ...