并发编程从零开始(十七)-ForkJoinPool(终章)
并发编程从零开始(十七)-ForkJoinPool(终章)
22 ForkJoinTask的fork/join
如果局部队列、全局中的任务全部是相互独立的,就很简单了。但问题是,对于分治算法来说,分解出来的一个个任务并不是独立的,而是相互依赖,一个任务的完成要依赖另一个前置任务的完成。
这种依赖关系是通过ForkJoinTask中的join()来体现的。且看前面的代码:
线程在执行当前ForkJoinTask的时候,产生了left、right 两个子Task。
fork是指把这两个子Task放入队列里面。
join则是要等待2个子Task完成。
而子Task在执行过程中,会再次产生两个子Task。如此层层嵌套,类似于递归调用,直到最底层的Task计算完成,再一级级返回。
22.1 fork
fork()的代码很简单,就是把自己放入当前线程所在的局部队列中。
如果是外部线程调用fork方法,则直接将任务添加到共享队列中。
22.2 join的嵌套
1.join的层层嵌套阻塞原理
join会导致线程的层层嵌套阻塞,如图所示:
线程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()。
2. ForkJoinTask的状态解析
要实现fork()/join()的这种线程间的同步,对应的ForkJoinTask一定是有各种状态的,这个状态变量是实现fork/join的基础。
初始时,status=0。共有五种状态,可以分为两大类:
-
未完成:status>=0。
-
已完成:status<0。
所以,通过判断是status>=0,还是status<0,就可知道任务是否完成,进而决定调用join()的线程是否需要被阻塞。
3. join的详细实现
下面看一下代码的详细实现。
getRawResult()是ForkJoinTask中的一个模板方法,分别被RecursiveAction和RecursiveTask实现,前者没有返回值,所以返回null,后者返回一个类型为V的result变量。
阻塞主要发生在上面的doJoin()方法里面。在dojoin()里调用t.join()的线程会阻塞,然后等待任务t执行完成,再唤醒该阻塞线程,doJoin()返回。
注意:当 doJoin()返回的时候,就是该任务执行完成的时候,doJoin()的返回值就是任务的完成状态,也就是上面的几种状态。
上面的返回值可读性比较差,变形之后:
先看一下externalAwaitDone(),即外部线程的阻塞过程,相对简单。
内部Worker线程的阻塞,即上面的wt.pool.awaitJoin(w, this, 0L),相比外部线程的阻塞要做更多工作。它现不在ForkJoinTask里面,而是在ForkJoinWorkerThread里面。
上面的方法有个关键点:for里面是死循环,并且只有一个返回点,即只有在task.status<0,任务完成之后才可能返回。否则会不断自旋;若自旋之后还不行,就会调用task.internalWait(ms);阻塞。
task.internalWait(ms);的代码如下。
4. join的唤醒
调用t.join()之后,线程会被阻塞。接下来看另外一个线程在任务t执行完毕后如何唤醒阻塞的线程。
任务的执行发生在doExec()方法里面,任务执行完成后,调用一个setDone()通知所有等待的线程。这里也做了两件事:
-
把status置为完成状态。
-
如果s != 0,即 s = SIGNAL,说明有线程正在等待这个任务执行完成。调用Java原生的notifyAll()通知所有线程。如果s = 0,说明没有线程等待这个任务,不需要通知。
23 ForkJoinPool的优雅关闭
同ThreadPoolExecutor一样,ForkJoinPool的关闭也不可能是“瞬时的”,而是需要一个平滑的过渡过程。
23.1 工作线程的退出
对于一个Worker线程来说,它会在一个for循环里面不断轮询队列中的任务,如果有任务,则执行,处在活跃状态;如果没有任务,则进入空闲等待状态。
这个线程如何退出呢?
(int) (c = ctl) < 0,即低32位的最高位为1,说明线程池已经进入了关闭状态。但线程池进入关闭状态,不代表所有的线程都会立马关闭。
23.2 shutdown()与shutdownNow()的区别
二者的代码基本相同,都是调用tryTerminate(boolean, boolean)方法,其中一个传入的是false,另一个传入的是true。tryTerminate意为试图关闭ForkJoinPool,并不保证一定可以关闭成功:
总结:shutdown()只拒绝新提交的任务;shutdownNow()会取消现有的全局队列和局部队列中的任务,同时唤醒所有空闲的线程,让这些线程自动退出。