并发编程从零开始(十七)-ForkJoinPool(终章)

并发编程从零开始(十七)-ForkJoinPool(终章)

22 ForkJoinTask的fork/join

如果局部队列、全局中的任务全部是相互独立的,就很简单了。但问题是,对于分治算法来说,分解出来的一个个任务并不是独立的,而是相互依赖,一个任务的完成要依赖另一个前置任务的完成。

这种依赖关系是通过ForkJoinTask中的join()来体现的。且看前面的代码:

image-20211104110004006

线程在执行当前ForkJoinTask的时候,产生了left、right 两个子Task。

fork是指把这两个子Task放入队列里面。

join则是要等待2个子Task完成。

而子Task在执行过程中,会再次产生两个子Task。如此层层嵌套,类似于递归调用,直到最底层的Task计算完成,再一级级返回。

22.1 fork

fork()的代码很简单,就是把自己放入当前线程所在的局部队列中。

如果是外部线程调用fork方法,则直接将任务添加到共享队列中。

image-20211104110116383


22.2 join的嵌套

1.join的层层嵌套阻塞原理

join会导致线程的层层嵌套阻塞,如图所示:

image-20211104110209448

线程1在执行 ForkJoinTask1,在执行过程中调用了 forkJoinTask2.join(),所以要等ForkJoinTask2完成,线程1才能返回;

线程2在执行ForkJoinTask2,但由于调用了forkJoinTask3.join(),只有等ForkJoinTask3完成后,线程2才能返回;

线程3在执行ForkJoinTask3。

结果是:线程3首先执行完,然后线程2才能执行完,最后线程1再执行完。所有的任务其实组成一个有向无环图DAG。如果线程3调用了forkJoinTask1.join(),那么会形成环,造成死锁。

那么,这种层次依赖、层次通知的 DAG,在 ForkJoinTask 内部是如何实现的呢?站在ForkJoinTask的角度来看,每个ForkJoinTask,都可能有多个线程在等待它完成,有1个线程在执行它。所以每个ForkJoinTask就是一个同步对象,线程在调用join()的时候,阻塞在这个同步对象上面,执行完成之后,再通过这个同步对象通知所有等待的线程。

利用synchronized关键字和Java原生的wait()/notify()机制,实现了线程的等待-唤醒机制。调用join()的这些线程,内部其实是调用ForkJoinTask这个对象的wait();执行该任务的Worker线程,在任务执行完毕之后,顺便调用notifyAll()。

image-20211104110254446

2. ForkJoinTask的状态解析

要实现fork()/join()的这种线程间的同步,对应的ForkJoinTask一定是有各种状态的,这个状态变量是实现fork/join的基础。

image-20211104110322980

初始时,status=0。共有五种状态,可以分为两大类:

  1. 未完成:status>=0。

  2. 已完成:status<0。

所以,通过判断是status>=0,还是status<0,就可知道任务是否完成,进而决定调用join()的线程是否需要被阻塞。

3. join的详细实现

下面看一下代码的详细实现。

image-20211104110353987

getRawResult()是ForkJoinTask中的一个模板方法,分别被RecursiveAction和RecursiveTask实现,前者没有返回值,所以返回null,后者返回一个类型为V的result变量。

image-20211104110415300

image-20211104110421558

阻塞主要发生在上面的doJoin()方法里面。在dojoin()里调用t.join()的线程会阻塞,然后等待任务t执行完成,再唤醒该阻塞线程,doJoin()返回。

注意:当 doJoin()返回的时候,就是该任务执行完成的时候,doJoin()的返回值就是任务的完成状态,也就是上面的几种状态。

image-20211104110444189

上面的返回值可读性比较差,变形之后:

image-20211104110455017

先看一下externalAwaitDone(),即外部线程的阻塞过程,相对简单。

image-20211104110509077

内部Worker线程的阻塞,即上面的wt.pool.awaitJoin(w, this, 0L),相比外部线程的阻塞要做更多工作。它现不在ForkJoinTask里面,而是在ForkJoinWorkerThread里面。

image-20211104110541313

image-20211104110549707

上面的方法有个关键点:for里面是死循环,并且只有一个返回点,即只有在task.status<0,任务完成之后才可能返回。否则会不断自旋;若自旋之后还不行,就会调用task.internalWait(ms);阻塞。

task.internalWait(ms);的代码如下。

image-20211104110610307

4. join的唤醒

调用t.join()之后,线程会被阻塞。接下来看另外一个线程在任务t执行完毕后如何唤醒阻塞的线程。

image-20211104110633497

image-20211104110640648

任务的执行发生在doExec()方法里面,任务执行完成后,调用一个setDone()通知所有等待的线程。这里也做了两件事:

  1. 把status置为完成状态。

  2. 如果s != 0,即 s = SIGNAL,说明有线程正在等待这个任务执行完成。调用Java原生的notifyAll()通知所有线程。如果s = 0,说明没有线程等待这个任务,不需要通知。


23 ForkJoinPool的优雅关闭

同ThreadPoolExecutor一样,ForkJoinPool的关闭也不可能是“瞬时的”,而是需要一个平滑的过渡过程。

23.1 工作线程的退出

对于一个Worker线程来说,它会在一个for循环里面不断轮询队列中的任务,如果有任务,则执行,处在活跃状态;如果没有任务,则进入空闲等待状态。

这个线程如何退出呢?

image-20211104110729302

image-20211104110739404

(int) (c = ctl) < 0,即低32位的最高位为1,说明线程池已经进入了关闭状态。但线程池进入关闭状态,不代表所有的线程都会立马关闭。


23.2 shutdown()与shutdownNow()的区别

image-20211104110846777

二者的代码基本相同,都是调用tryTerminate(boolean, boolean)方法,其中一个传入的是false,另一个传入的是true。tryTerminate意为试图关闭ForkJoinPool,并不保证一定可以关闭成功:

总结:shutdown()只拒绝新提交的任务;shutdownNow()会取消现有的全局队列和局部队列中的任务,同时唤醒所有空闲的线程,让这些线程自动退出。


posted @ 2021-11-04 11:33  会编程的老六  阅读(582)  评论(0编辑  收藏  举报