ForkJoin详解之实践篇

354 阅读3分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第21天,点击查看活动详情

一、ForkJoin框架中一些重要的类图

image.png

二、ForkJoinPool类

ForkJoinTask需要通过ForkJoinPool来执行.它是一个线程池,使用了一个无限队列来保存需要执行的任务,而线程的数量则是通过构造函数传入,如果没有向构造函数中传入希望的线程数量,那么当前计算机可用的CPU数量会被设置为线程数量作为默认值。

ForkJoinPool类实现了线程池的Executor接口。可以使用Executors.newWorkStealPool()方法创建ForkJoinPool。ForkJoinPool执行任务分割出的子任务会添加到当前工作线程所维护的双端队列中,进入队列的头部。当一个工作线程的队列里暂时没有任务时,它会随机从其他工作线程的队列的尾部获取一个任务。

主要方法:

//excute方法是异步的
public void execute(ForkJoinTask<?> task)
public void execute(Runnable task)

//方法是同步阻塞的
public <T> T invoke(ForkJoinTask<T> task)
public <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks) 
public <T> ForkJoinTask<T> submit(ForkJoinTask<T> task)
public <T> ForkJoinTask<T> submit(Callable<T> task)
public <T> ForkJoinTask<T> submit(Runnable task, T result)
public ForkJoinTask<?> submit(Runnable task)

三、ForkJoinWorkerThread类

ForkJoinWorkerThread继承自Thread,受ForkJoinPool支配用以执行ForkJoinTaskForkJoinWorkerThread为任务的执行线程workers数组在构造方法中初始化,其大小必须为2的n次方(方便将取模转换为移位运算)。

ForkJoinPool可以通过execute提交ForkJoinTask任务,然后通过ForkJoinWorkerThread. pushTask实现添加任务。

final void pushTask(ForkJoinTask<?> t) {
    ForkJoinTask<?>[] q
    int s, m;
    if ((q = queue) != null) { 
        // ignore if queue removed
        long u = (((s = queueTop) & (m = q.length - 1)) << ASHIFT) + ABASE;
        UNSAFE.putOrderedObject(q, u, t);
        queueTop = s + 1
        // or use putOrderedInt
        if ((s -= queueBase) <= 2)
            pool.signalWork();
        else if (s == m)
            growQueue();
    }
}
首先将任务放在`queueTop`指向的队列位置,再将`queueTop`加1。
然后分析队列容量情况,当数组元素比较少时(1或者2),就调用`signalWork()`方法。`signalWork()`方法做了两件事:
-   唤醒当前线程;
-   当没有活动线程时或者线程数较少时,添加新的线程。

`else if`部分表示队列已满(队头指针=队列长度减1),调用`growQueue()`扩容。

ForkJoinWorkerThread使用数组实现双端队列,用来盛放ForkJoinTaskqueueTop指向对头,queueBase指向队尾。本地线程插入任务、获取任务都在队头进行,其他线程“窃取”任务则在队尾进行。

poolIndex本线程在ForkJoinPool中工作线程数组中的下标,stealHint保存了最近的窃取者(来窃取任务的工作线程)的下标(poolIndex)。注意这个值不准确,因为可能同时有很多窃取者来窃取任务,这个值只能记录其中之一。

四、ForkJoinTask类

ForkJoinTask封装了数据及其相应的计算,并且支持细粒度的数据并行。ForkJoinTask比线程要轻量,ForkJoinPool中少量工作线程能够运行大量的ForkJoinTask。

ForkJoinTask类中主要包括两个方法fork()和join(),分别实现任务的分拆与合并。

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

可见,fork()操作是通过调用`ForkJoinWorkerThread.pushTask()`实现的
public final V join() {
if (doJoin() != NORMAL)
return reportResult();
else
return getRawResult();
}

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

它调用了`doJoin()`方法,通过`doJoin()`方法得到当前任务的状态来判断返回什么结果,任务状态有四种


private static final int NORMAL = -1;
private static final int CANCELLED = -2;
private static final int EXCEPTIONAL = -3;
private static final int SIGNAL = 1;

-   如果任务状态是已完成,则直接返回任务结果。
-   如果任务状态是被取消,则直接抛出`CancellationException`。
-   如果任务状态是抛出异常,则直接抛出对应的异常。

fork()方法类似于Thread.start(),但是它并不立即执行任务,而是将任务放入工作队列中。跟Thread.join()方法不同,ForkJoinTask的join()方法并不简单的阻塞线程,而是利用工作线程运行其他任务,当一个工作线程中调用join(),它将处理其他任务,直到注意到目标子任务已经完成。

ForkJoinTask有3个子类:

  • RecursiveAction:用于没有返回结果的任务。
  • RecursiveTask:用于有返回结果的任务。
  • CountedCompleter:完成任务后将触发其他任务。

RecursiveTask 类

有返回结果的ForkJoinTask实现Callable。

public class ForkJoinTest extends RecursiveTask<Integer> {

    private static final int THRESHHOLD = 2;
    private int start;
    private int end;

    public ForkJoinTest(int start, int end) {
        this.start = start;
        this.end = end;
    }

    @Override
    protected Integer compute() {
        System.out.println(start + " - " + end + " begin");
        int sum = 0;
        boolean canCompute = (end - start) <= THRESHHOLD;
        if (canCompute) { // 达到了计算条件,则直接执行
            for (int i = start; i <= end; i++) {
                sum += i;
            }
        } else { // 不满足计算条件,则分割任务
            int middle = (start + end) / 2;

            ForkJoinTest leftTask = new ForkJoinTest(start, middle);
            ForkJoinTest rightTask = new ForkJoinTest(middle + 1, end);

            leftTask.fork(); // 执行子任务
            rightTask.fork();// 执行子任务
            int leftResult = leftTask.join(); // 等待子任务执行完毕
            int rightResult = rightTask.join();// 等待子任务执行完毕

            sum = leftResult + rightResult; // 合并子任务的计算结果
        }
        System.out.println(start + + end + " end");
        return sum;
    }

    public static void main(String[] args) throws InterruptedException, ExecutionException {
        ForkJoinPool pool = new ForkJoinPool();
        ForkJoinTest task = new ForkJoinTest(1, 8);
        Future<Integer> future = pool.submit(task);
        if (task.isCompletedAbnormally()) {
            System.out.println(task.getException());
        } else {
            System.out.println("result: " + future.get());
        }
    }

}

RecursiveAction类

无返回结果的ForkJoinTask实现Runnable。

遍历目录搜寻目录下的所有文件,继承RecursiveAction实现无返回值的任务的执行:

public class FindFiles extends RecursiveAction {

//要搜寻的目录
private File dir;

public FindFiles(File dir) {
    this.dir = dir;
}

@Override
protected void compute() {
    File[] files = dir.listFiles();
    if (files != null) {
        List<FindFiles> list = new ArrayList<>();
        for (File file : files) {
            //如果是目录,就需要分割任务,交给ForkJoinPool去执行,因为任务数目不确定,所以需要定义一个集合
            if (file.isDirectory()) {
                FindFiles findFiles = new FindFiles(file);
                list.add(findFiles);


                //不是目录,是文件就执行自己的逻辑
            } else {
                if (file.getAbsolutePath().endsWith("dll")) {
                    System.out.println(file.getAbsolutePath());
                }
            }
        }
        //如果任务
        if (list.size() > 0) {
            Collection<FindFiles> findFiles = invokeAll(list);
            for (FindFiles findFiles1 : findFiles) {
                //等待所有的任务执行完成
                findFiles1.join();

                //所有的任务都执行完了才会执行
                System.out.println(Thread.currentThread().getName() + "....join end..");
            }
        }
    }
}


private static void testFork() {
    ForkJoinPool forkJoinPool = new ForkJoinPool();
    FindFiles findFiles = new FindFiles(new File("d://"));

    //execute方法是异步的
    forkJoinPool.execute(findFiles);

    //阻塞,等待ForkJoin执行完,主线程才往下执行
    findFiles.join();

    System.out.println("end.....");
}


public static void main(String[] args) {
    testFork();
}

CountedCompleter 类

在任务完成执行后会触发执行一个自定义的钩子函数。

五、使用场景:

Fork/Join框架适合能够进行拆分再合并的计算密集型(CPU密集型)任务。ForkJoin框架是一个并行框架,因此要求服务器拥有多CPU、多核,用以提高计算能力。