ForkJoinPool.invoke()和ForkJoinTask.invoke()或compute()

时间:2015-12-07 11:20:13

标签: java concurrency java.util.concurrent fork-join forkjoinpool

我正在阅读Java ForkJoin框架。如果没有直接针对invoke()(例如ForkJoinTask)的实施调用RecursiveTask,而是实例化ForkJoinPool并致电pool.invoke(task),还会带来哪些额外好处?当我们称这两种方法都称为invoke时会发生什么?

从源代码看,似乎如果调用recursiveTask.invoke,它将以托管线程池方式调用其exec并最终调用compute。因此,为什么我们有成语pool.invoke(task)更令人困惑。

我写了一些简单的代码来测试性能差异,但我没有看到任何代码。也许测试代码错了?见下文:

public class MyForkJoinTask extends RecursiveAction {

    private static int totalWorkInMillis = 20000;
    protected static int sThreshold = 1000;

    private int workInMillis;


    public MyForkJoinTask(int work) {
        this.workInMillis = work;
    }

    // Average pixels from source, write results into destination.
    protected void computeDirectly() {
        try {

            ForkJoinTask<Object> objectForkJoinTask = new ForkJoinTask<>();
            Thread.sleep(workInMillis);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    @Override
    protected void compute() {
        if (workInMillis < sThreshold) {
            computeDirectly();
            return;
        }

        int discountedWork = (int) (workInMillis * 0.9);
        int split = discountedWork / 2;

        invokeAll(new MyForkJoinTask(split),
                new MyForkJoinTask(split));
    }

    public static void main(String[] args) throws Exception {
        System.out.printf("Total work is %d in millis.%n", totalWorkInMillis);
        System.out.printf("Threshold is %d in millis.%n", sThreshold);

        int processors = Runtime.getRuntime().availableProcessors();
        System.out.println(Integer.toString(processors) + " processor"
                + (processors != 1 ? "s are " : " is ")
                + "available");

        MyForkJoinTask fb = new MyForkJoinTask(totalWorkInMillis);

        ForkJoinPool pool = new ForkJoinPool();

        long startTime = System.currentTimeMillis();


        // These 2 seems no difference!
        pool.invoke(fb);
//        fb.compute();


        long endTime = System.currentTimeMillis();

        System.out.println("Took " + (endTime - startTime) +
                " milliseconds.");
    }
}

1 个答案:

答案 0 :(得分:5)

compute()类的RecursiveTask方法只是一个包含任务代码的抽象方法。它没有使用池中的新线程,如果你正常调用它,它就不会在池托管线程中运行。

fork join pool上的invoke方法将任务提交给池,然后池开始在单独的线程上运行,在该线程上调用compute方法,然后等待结果。

你可以在java doc for RecursiveTask和ForkJoinPool的措辞中看到这一点。 invoke()方法实际执行任务,而compute()方法只是封装计算。

protected abstract V compute()
     

此任务执行的主要计算。

和ForkJoinPool

public <T> T invoke(ForkJoinTask<T> task)
     

执行给定任务,完成后返回结果。 ...

因此,使用compute方法,您正在执行的操作是在fork join pool之外运行对compute的第一次调用。您可以通过在compute方法中添加日志行来测试它。

System.out.println(this.inForkJoinPool());

您还可以通过记录线程ID

来检查它是否在同一个线程中运行
System.out.println(Thread.currentThread().getId());

调用invokeAll后,该调用中包含的子任务将在池中运行。但请注意,它不一定在您调用compute()之前创建的池中运行。您可以注释掉new ForkJoinPool()代码,它仍会运行。有趣的是,java 7 doc说如果invokeAll()方法在池托管线程之外被调用,它会引发异常,但java 8 doc没有。我还没有在java 7中测试它(只有8个)。但很可能,在java 7中直接调用compute()时,您的代码会抛出异常。

两个结果同时返回的原因是,毫秒不足以准确记录在池托管线程中启动第一个线程的差异,或者仅运行第一个compute调用一个现有的线程。

Sierra和Bates建议您使用fork join框架的OCA / OCP学习指南的方法是从池中调用invoke()。它清楚地表明您正在使用哪个池,这也意味着您可以将多个任务提交到同一个池,这样可以节省每次重新创建新池的开销。从逻辑上讲,将所有任务计算保存在池托管线程中(或者至少我认为是这样),它也更清晰。