ForkJoin简介
Java7中新添加了一种并发框架ForkJoin.如其名所言,ForkJoin框架适用于将任务分解为子任务,然后将结果合并的场景.
ForkJoin与Executor类似,使用池(ForkJoinPool)来管理线程(ForkJoinWorkerThread),使用队列来管理任务(ForkJoinTask).
不同的是,ForkJoin中使用多个队列来管理任务,并且分为共享队列(偶数)和工作队列(奇数).
共享队列只是用来存储提交的任务.
工作队列会与一个工作者线程ForkJoinWorkerThread绑定.
在工作队列为空时,工作者线程会从其他任务队列(包含共享和工作队列)窃取一个任务来执行.
工作者线程执行并分解任务,其分解的任务,会提交到其对应的工作队列.
ForkJoin工作流程
ForkJoinPool工作流程
ForkJoinPool工作流程如下图:
步骤说明如下:
1.任务task0被提交到ForkJoinPool池
2.ForkJoinPool池将任务放入共享队列(偶数队列)
3.检查并发等级(活动工作线程数),ForkJoinPool池添加工作队列和工作者线程
4.工作者线程遍历ForkJoinPool池中的任务队列,取出一个任务.
5.工作者线程发现共享队列除了取出的任务外,还有其他任务,通知ForkJoinPool池创建新工作者;然后执行任务(任务执行中可能产生新任务,新任务提交到此线程对应的工作队列);执行任务达到一定数量或工作者队列为空时,尝试等待或退出,不满足条件重复4,5步骤;满足等待,则停止工作,等待唤醒,唤醒后重复4,5步骤;满足退出条件,则退出.
6.ForkJoinPool池根据并发等级等条件创建新工作队列和工作者线程
7.新工作者线程遍历ForkJoinPool池中的队列,取出一个任务;重复步骤5的操作.
线程工作窃取流程
任务在等待子任务完成,即执行subTask.join()时,进行工作窃取.流程如图:
1.w工作者,在等待currentJoin任务的完成;
2.它遍历工作者队列,发现v工作者在执行此任务(即v.currentSteal == w.currentJoin), 并且v队列的数组中还有其他任务,w则帮助v执行任务,如果w帮助v将队列数组中的任务执行完了,而w等待的currentJoin还没有完成,则w检查v是否在等待其他任务;
3.如果在等待(即v.currentJoin != null),则找到对应执行任务的j(即j.currentSteal == v.currentJoin);不在等待,或没有对应的j,进入步骤5
4.如果j队列的数组中还有其他任务,w则帮助j执行任务;如果没有任务,且j的任务没有执行完成,则检查j是否在等待其他任务,如果有,反复类似3,4操作;
5.否则,w请求睡眠补偿(即w通知池它将睡眠,池启动另一个工作者替代).如果成功,则w睡眠;否则继续遍历工作者队列(如前面ForkJoinPool工作流程中4)
工作者线程睡眠唤醒
工作者线程的睡眠唤醒是通过ForkJoinPool的成员域ctl控制的.
如下图:
如前所述,工作者线程是绑定到一个工作队列的.
工作者线程在睡眠前,会将其对应工作队列的scanState写入ctl的低32位.而原ctl中的低32的值会写到工作队列的stackPred中.这样就形成了一个链表.ctl低32位对应最后睡眠的工作者线程t0的工作队列wt0的scanState;而工作队列wt0的stackPred对应在其之前睡眠的工作者线程t1(如果存在)的工作队列wt1的 scanState;依次类推,即可遍历出所有线程睡眠的反序列. 而线程唤醒顺序即为这个反序列.
其他
为了提高性能,在一些小细节上有一些特别的优化.
如,为了判断是否要新建线程,即活动运行工作者数量是否达到了目标并发数,它不是保存活动运行工作者数量,而是保存活动运行工作者数减去目标并发(AC)的数值,这样每次判断时不用再进行减法操作.
还有,它在一些地方使用了延时锁.这样即保证了数据的同步,又减小了同步开销.