我正在阅读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.");
}
}
答案 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()
。它清楚地表明您正在使用哪个池,这也意味着您可以将多个任务提交到同一个池,这样可以节省每次重新创建新池的开销。从逻辑上讲,将所有任务计算保存在池托管线程中(或者至少我认为是这样),它也更清晰。