Fork/Join

本文介绍了Java的Fork/Join框架,它采用分治思想将大任务拆分为子任务并行处理。ForkJoinPool是执行ForkJoinTask的核心,提供fork()和join()方法进行任务的拆分和结果合并。异常处理方面,isCompletedAbnormally()检查任务异常,getException()获取异常。注意事项包括避免不适当的join调用,禁止在RecursiveTask内部使用invoke,以及理解工作窃取的概念以实现负载均衡。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

① Fork/Join 分治思想

Fork/Join 它可以将一个大的任务拆分成多个子任务进行并行处理,最后将子任务结果合并成最后的计算结果,并进行输出。Fork/Join 框架要完成两件事情:

Fork:把一个复杂任务进行分拆,大事化小

Join:把分拆任务的结果进行合并
在这里插入图片描述

  1. 任务分割:首先 Fork/Join 框架需要把大的任务分割成足够小的子任务,如果子任务比较大的话还要对子任务进行继续分割
  2. 执行任务并合并结果:分割的子任务分别放到双端队列里,然后几个启动线程分别从双端队列里获取任务执行。子任务执行完的结果都放在另外一个队列里,

启动一个线程从队列里取数据,然后合并这些数据。

在 Java 的 Fork/Join 框架中,使用两个类完成上述操作 ForkJoinTask:我们要使用 Fork/Join 框架,首先需要创建一个 ForkJoin 任务。 该类提供了在任务中执行 fork 和 join 的机制。通常情况下我们不需要直接集成 ForkJoinTask 类,只需要继承它的子类,Fork/Join 框架提供了两个子类:

  1. RecursiveAction:用于没有返回结果的任务
    在这里插入图片描述

  2. RecursiveTask:用于有返回结果的任务
    在这里插入图片描述

②ForkJoinPool

ForkJoinPool:ForkJoinTask 需要通过 ForkJoinPool 来执行

RecursiveTask: 继承后可以实现递归(自己调自己)调用的任务
在这里插入图片描述

public ForkJoinTask submit(ForkJoinTask task)
提交一个ForkJoinTask来执行。
参数类型
T - 任务结果的类型
参数
task - 提交的任务
结果
任务
异常
NullPointerException - 如果任务为空
RejectedExecutionException - 如果任务无法安排执行

③ Fork/Join 框架的实现原理

ForkJoinPool 由 ForkJoinTask 数组和 ForkJoinWorkerThread 数组组成,ForkJoinTask 数组负责存放以及将程序提交给 ForkJoinPool,而 ForkJoinWorkerThread 负责执行这些任务。

在这里插入图片描述

fork()方法

public final ForkJoinTask fork()
在当前任务正在运行的池中异步执行此任务(如果适用),或使用ForkJoinPool.commonPool()(如果不是inForkJoinPool())进行异步执行 。 虽然它不一定执行,但是除非完成并重新初始化,否则任务多次分配是一种使用错误。 对此任务的状态的后续修改或其操作的任何数据不一定被除了执行它之外的任何线程一致地观察到,除非在join()或相关方法的呼叫之前,或者呼叫isDone()返回true 。

join()方法

public final V join()
返回计算时,它的结果is done 。 该方法与get()的不同之处在于,异常完成导致RuntimeException或Error ,而不是ExecutionException ,并且调用线程的中断不会导致该方法通过投掷InterruptedException突然返回。

Fork 方法的实现原理: 当我们调用 ForkJoinTask 的 fork 方法时,程序会把任务放在 ForkJoinWorkerThread 的 workQueue 中,异步地执行这个任务,然后立即返回结果

在这里插入图片描述

Join方法的实现原理

Join 方法的主要作用是阻塞当前线程并等待获取结果。

它首先调用 doJoin 方法,
在这里插入图片描述

通过 doJoin()方法中的doExec()方法得到当前任务的状态来判断返回什么结果,
在这里插入图片描述
任务状态常见 4 种:

已完成(NORMAL)、被取消(CANCELLED)、信号(SIGNAL)和出现异常(EXCEPTIONAL)

• 如果任务状态是已完成,则直接返回任务结果。

• 如果任务状态是被取消,则直接抛出 CancellationException

• 如果任务状态是抛出异常,则直接抛出对应的异常
在这里插入图片描述

④ Fork/Join 框架的异常处理

ForkJoinTask 在执行的时候可能会抛出异常,但是我们没办法在主线程里直接捕获异常,所以 ForkJoinTask 提供了 isCompletedAbnormally()方法来检查任务是否已经抛出异常或已经被取消了,

public final boolean isCompletedAbnormally()
如果此任务抛出异常或被取消,则返回 true 。
结果
true如果此任务抛出异常或被取消

isCompletedAbnormally()方法:

 public final boolean isCompletedAbnormally() {
        return status < NORMAL;//已完成
    }

并且可以通过 ForkJoinTask 的 getException 方法获取异常。

public final Throwable getException()
返回由基础计算抛出的异常,或 CancellationException取消,如果,或 null如果没有,或者如果方法尚未完成。
结果
例外,或 null如果没有

getException()方法:

  public final Throwable getException() {
        int s = status & DONE_MASK;
        //先判断是否已经完成,如果完成返回null,
        //如果被取消,则抛出CancellationException异常
        return ((s >= NORMAL)    ? null :
                (s == CANCELLED) ? new CancellationException() :
                getThrowableException());//如果任务状态是抛出异常,则直接抛出对应的异常
    }

getException 方法返回 Throwable 对象,如果任务被取消了则返回 CancellationException。如果任务没有完成或者没有抛出异常则返回 null。

案例

场景: 生成一个计算任务,计算 1+2+3…+1000,每 100 个数切分一个子任务

// RecursiveTask 继承后可以实现递归(自己调自己)调用的任务 
class TaskExample extends RecursiveTask<Long> {
    private int start;
    private int end;
    private long sum;

    /**
     * 构造函数
     *
     * @param start
     * @param end
     */
    public TaskExample(int start, int end) {
        this.start = start;
        this.end = end;
    }

    @Override
    protected Long compute() {
        //System.out.println("任务" + start + "=========" + end + "累加开始");
        //大于 10 个数相加切分,小于直接加
        if (end - start <= 10) {
            for (int i = start; i <= end; i++) {
                //累加
                sum += i;
            }
        } else {
            //切分为 2 块
            int middle = (start+end)/2;
            //递归调用,切分为 2 个小任务
            TaskExample taskExample1 = new TaskExample(start, middle);
            TaskExample taskExample2 = new TaskExample(middle + 1, end);
            //执行:异步
            taskExample1.fork();
            taskExample2.fork();
            //同步阻塞获取执行结果
            sum = taskExample1.join() + taskExample2.join();
        }
        //加完返回
        return sum;
    }
}
public class ForkJoinPoolDemo {

    public static void main(String[] args) {
        //定义任务
        TaskExample taskExample = new TaskExample(1, 100);
        //定义执行对象
        ForkJoinPool forkJoinPool = new ForkJoinPool();
        //加入任务执行
        ForkJoinTask<Long> result = forkJoinPool.submit(taskExample);
        //输出结果
        try {
            System.out.println(result.get());
        }catch (Exception e){
            e.printStackTrace();
        }finally {
        	//关闭池
            forkJoinPool.shutdown();
        }
    } }

get()方法获取结果

public final V get()
throws InterruptedException,
ExecutionException
等待计算完成,然后检索其结果。
Specified by:
get在界面 Future
结果
计算结果
异常
CancellationException - 如果计算被取消
ExecutionException - 如果计算抛出异常
InterruptedException - 如果当前线程不是ForkJoinPool的成员,并且在等待时中断

shutdown()方法关闭ForkJoinPool池

public void shutdown()
可能启动有序关闭,其中先前提交的任务被执行,但不会接受任何新的任务。 如果这是commonPool() ,调用对执行状态没有影响,如果已经关闭,则不起作用。 在此方法过程中同时提交的任务可能会被拒绝,也可能不会被拒绝。
异常
SecurityException - 如果安全管理器存在,并且主叫方不允许修改线程,因为它不保留RuntimePermission (“modifyThread”)

⑤ Fork/Join注意事项

1)对一个任务调用 join 方法会阻塞调用方,直到该任务做出结果。因此,有必要在两个子任务的计算都开始之后再调用它。否则,你得到的版本会比原始的顺序算法更慢更复杂,因为每个子任务都必须等待另一个子任务完成才能启动。
2)不应该在 RecursiveTask 内部使用 ForkJoinPool 的 invoke 方法。相反,你应该始终直接调用 compute 或 fork 方法,只有顺序代码才应该用 invoke 来启动并行计算。
3) 对子任务调用 fork 方法可以把它排进 ForkJoinPool 。同时对左边和右边的子任务调用fork()似乎很自然,但这样做的效率要比直接对其中一个调用 compute 低。调用compute你可以为其中一个子任务重用同一线程,从而避免在线程池中多分配一个任务造成的开销。
4)和并行流一样,你不应理所当然地认为在多核处理器上使用分支/合并框架就比顺序计算快。一个任务可以分解成多个独立的子任务,才能让性能在并行化时有所提升。所有这些子任务的运行时间都应该比分出新任务所花的时长

⑥ Fork/Join 工作窃取思想

在理想的情况下,每个任务完成的时间应该是相同的,这样在多核cpu的前提下,我们能保证每个核处理的时间都是相同的。
实际情况中,每个子任务花费的时间可以说是天差地别,磁盘,网络,或等等很多的因素导致。
Fork/Join框架为了解决这个提出,提出了工作窃取(work stealing)的概念。
在实际应用中,这意味着这些任务差不多被平均分配到 ForkJoinPool 中的所有线程上。每个线程都为分配给它的任务保存一个双向链式队列,每完成一个任务,就会从队列头上取出下一个任务开始执行。
基于前面所述的原因,某个线程可能早早完成了分配给它的所有任务,也就是它的队列已经空了,而其他的线程还很忙。这时,这个线程并没有闲下来,而是随机选了一个别的线程,从队列的尾巴上“偷走”一个任务。这个过程一直继续下去,直到所有的任务都执行完毕,所有的队列都清空。这就是为什么要划成许多小任务而不是少数几个大任务,这有助于更好地在工作线程之间平衡负载。
在这里插入图片描述
工作窃取算法优缺点
优点:就是充分利用线程进行并行计算,减少了线程间的竞争。
缺点:当双端队列里只有一个任务时,这时会存在竞争。而且该算法创建多个线程和多个双端队列, 会消耗更过的系统资源。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值