ForkJoin框架(二):ForkJoinTask

1. ForkJoinTask概述

在ForkJoin框架概述中已经对ForkJoinTask进行了基本介绍,本文主要从实现的角度剖析ForkJoinTask。简单的说,ForkJoinTask将任务fork成足够小的任务,并发解决这些小任务,然后将这些小任务结果join。这种思想充分利用了CPU的多核系统,使得CPU的利用率得到大幅度提升,减少了任务执行时间。

通常我们会利用ForkJoinTask的fork方法来分割任务,利用join方法来合并任务,因此我们首先以这两个方法作为切入点。

2. 分割子任务-fork

public final ForkJoinTask fork() {
    ((ForkJoinWorkerThread) Thread.currentThread()).pushTask(this);
    return this;
}

该方法很简单,将该任务添加到当前线程维护的队列队首处,返回该任务。这里有三点需要注意的:

  1. fork方法将当前线程强制转换成ForkJoinWorkerThread,通过ForkJoinPool执行ForkJoinTask的线程都是该框架定义的ForkJoinWorkerThread,因此这种转换是正确的。如果不是利用ForkJoinPool线程池执行ForkJoinTask,将Thread强制转换成ForkJoinWorkerThread会抛出ClassCastException异常。
  2. 将任务加到队列后,由ForkJoinPool调度执行该任务。一般情况下,该任务会被队列所在的线程执行,当线程池其他线程空闲的时候,该任务可能被其他线程窃取。
  3. 任务被添加到队列的队首处,即本线程fork的小任务被添加到队首,这隐藏着另一个意思,队列中越早的任务是个较大的任务,刚添加的任务是较小的任务,每次该线程从队首拿更小的任务执行,更小的任务执行完成之后才能join成更大的任务。其他线程从该队列窃取任务的时候会窃取队列最早的任务,即窃取了较大的任务执行。

3. 合并结果-join

假设我们将当前任务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。下面详细看下每个状态:

  1. NORMAL:表示任务“正常”完成的状态。
  2. CANCELLED:表示任务“取消”完成的状态。
  3. EXCEPTIONAL:表示任务“异常”完成的状态。注意,以上这三个状态都是“完成”的状态,只是完成的途径不一样。
  4. SIGNAL:有其他任务依赖当前任务,任务结束前,通知其他任务join当前任务的结果。
  5. 0:任务初始状态(正在执行状态),不需要等待子任务完成。

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该任务的线程通常是一个等待父任务完成的线程,也就是说,此时当前任务其实是个子任务,子任务结束后,父任务就可以尝试合并子任务的执行结果了,看下示例图:

ForkJoin框架(二):ForkJoinTask_第1张图片

任务执行过程抛出异常,调用setExceptionalCompletion方法设置异常完成状态,学习该方法源码之前我们先学习下ForkJoinTask的类变量exceptionTable和exceptionTableLock,内部静态类ExceptionNode:


//任务执行过程中抛出异常的ForkJoinTask弱引用数组,这是一个类变量,所有ForkJoinTask共用
//该数组
private static final ExceptionNode[] exceptionTable;
//异常任务table锁
private static final ReentrantLock exceptionTableLock;

//保存弱引用的queue,GC时将回收的对象对应的弱引用保存到该队列中
private static final ReferenceQueue exceptionTableRefQueue;

//ExceptionNode是一个弱引用,保存了执行过程中抛出异常的ForkJoinTask。
static final class ExceptionNode extends WeakReference>{
    final Throwable ex;
    ExceptionNode next;
    final long thrower; 
    ExceptionNode(ForkJoinTask task, Throwable ex, ExceptionNode next) {
      super(task, exceptionTableRefQueue);
      this.ex = ex;
      this.next = next;
      this.thrower = Thread.currentThread().getId();
    }
} 
  

任务执行过程抛出异常时,调用者可以获取该异常,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);
}

方法的注释已经很清楚了,这里再总结下该方法的处理逻辑:

  1. 根据exceptionTableRefQueue,删除exceptionTable中已经过期的ForkJoinTask弱引用。
  2. 将当前任务包装成ExceptionNode弱引用,并添加到哈希表exceptionTable中。
  3. 调用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方法的实现。

4. 取任务执行结果-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的构造函数,通过该构造函数和反射,创建一个该异常的实例并返回。

5. 取消任务-cancel

public boolean cancel(boolean mayInterruptIfRunning) {
    return setCompletion(CANCELLED) == CANCELLED;
}

该方法很简单,调用setCompletion方法将任务的状态设置为CANCELLED,如果设置成功说明取消任务成功,否则取消任务失败。

6. invoke

该方法等待任务执行完成,返回任务执行结果,或者抛出异常。

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);
}

7. 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();
}

该方法主要调用doJoin方法等待任务结束,根据任务结束的状态,决定抛出相应的异常,或者返回任务结果。

8. reinitialize

reinitialize方法重置任务的状态使得该任务可以被重新执行。

public void reinitialize() {
    if (status == EXCEPTIONAL)
      clearExceptionalCompletion();
    else
      status = 0;
}

该方法重置任务状态为0,如果有异常信息,清除异常信息。

9. getPool、inForkJoinPool

这两个方法非常简单,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;
}

10. tryUnfork

tryUnfork方法尝试将该任务从任务队列中弹出。该任务不再被线程池调度。

public boolean tryUnfork() {
    return ((ForkJoinWorkerThread) Thread.currentThread())
      .unpushTask(this);
}

11. getQueuedTaskCount

getQueuedTaskCount方法返回当前线程已经fork但是没有执行的任务数量。

public static int getQueuedTaskCount() {
    //返回任务队列中任务的数量
    return ((ForkJoinWorkerThread) Thread.currentThread())
      .getQueueSize();
}

好了,ForkJoinTask的主要源码分析到此为上。

你可能感兴趣的:(java)