在java8中流操作中,只有到终止符才会进行真正的求值。在设定了并行操作的时候,会对任务进行分解。
任务的分解用到了joinfork框架。
@Override
public void compute() {
Spliterator<P_IN> rs = spliterator, ls; // right, left spliterators
long sizeEstimate = rs.estimateSize();
long sizeThreshold = getTargetSize(sizeEstimate);
boolean forkRight = false;
@SuppressWarnings("unchecked") K task = (K) this;
while (sizeEstimate > sizeThreshold && (ls = rs.trySplit()) != null) {
K leftChild, rightChild, taskToFork;
task.leftChild = leftChild = task.makeChild(ls);
task.rightChild = rightChild = task.makeChild(rs);
task.setPendingCount(1);
if (forkRight) {
forkRight = false;
rs = ls;
task = leftChild;
taskToFork = rightChild;
}
else {
forkRight = true;
task = rightChild;
taskToFork = leftChild;
}
taskToFork.fork();
sizeEstimate = rs.estimateSize();
}
task.setLocalResult(task.doLeaf());
task.tryComplete();
}
以AbstractTask为例子,当流操作遇到终止符并设定了并行的时候,大部分情况都会执行AbstractTask的compute()方法。
首先会获取目标流的spliterator,可分割的迭代器,对于任务中的所要被分割操作的数据也将在这个迭代器中被产生子迭代器在子任务中进行相应的操作。
首先会通过当前迭代器的estimateSize()方法得到当前迭代器中所要处理的数据量,以ArrayList为例子,也就是将要迭代的数据最大下标减去最小下标。而后会根据这个数据量,计算每个任务的处理的最大数据量。
public static long suggestTargetSize(long sizeEstimate) {
long est = sizeEstimate / LEAF_TARGET;
return est > 0L ? est : 1L;
}
static final int LEAF_TARGET = ForkJoinPool.getCommonPoolParallelism() << 2;
可以看到这里的每个分片的最大数据处理量为要处理的数据总量除以LEAF_TARGET,而LEAF_TARGET的大小为forkjoin线程池大小左移两位也就是乘4,而forkjoin线程池的大小如下所设置。
if (parallelism < 0 && // default 1 less than #cores
(parallelism = Runtime.getRuntime().availableProcessors() - 1) <= 0)
parallelism = 1;
if (parallelism > MAX_CAP)
parallelism = MAX_CAP;
return new ForkJoinPool(parallelism, factory, handler, LIFO_QUEUE,
"ForkJoinPool.commonPool-worker-");
可以看到该线程池数量为cpu数量-1,也就是说每个任务的最大数据量为要处理的数据总量除以cpu数量-1的四倍。
在确定了每个任务的最大处理数据量,就是开始分割任务。
如果当前迭代器中要处理的数据总量大于最大分片量,那么就调用迭代器的trySplit()方法进行分割,得到子迭代器,并且原来的迭代器的数据也进行了分割。
在分割完毕之后,给当前任务生成左右两个子任务,左任务的迭代器则是经过分割得到的子迭代器,而右任务的迭代器则是经过分割后得到的原迭代器,这样的分配,也符合两个迭代器的下标内容,之后给当前的任务设置pending为1。
接下来会交替根据左右子任务继续进行分割,而并没有在下一次循环中被分割的子任务则是丢入forkjoin线程池中准备执行,并在其compute()方法中会并发继续进行分割。这里也就可以看到pending为1的目的,由于左右任务总有一个将继续在本线程中进行执行分割或者流操作,所以只需要等待一个子任务完成。
在完成任务的分割,保证当前任务的数据量不会再大于最大所允许的数据量之后,调用doleaf()方法得到当前任务的运算结果。
在完成当前任务的计算之后,调用tryCompletion()方法表示当前任务的结束。
public final void tryComplete() {
CountedCompleter<?> a = this, s = a;
for (int c;;) {
if ((c = a.pending) == 0) {
a.onCompletion(s);
if ((a = (s = a).completer) == null) {
s.quietlyComplete();
return;
}
}
else if (U.compareAndSwapInt(a, PENDING, c, c - 1))
return;
}
}
首先会得到当前任务的pending,如果不为0则会直接减去1,如果为0,说明此时该任务需要进行关闭,则会先调用自己的onCompletion(),方法,之后不断向上尝试给自己父任务的pending减一。
如果为0,说明此时该任务的所有子任务已经完毕,或者当前任务是叶子任务并且已经完成。那么调用当前的onComplection()方法,准备任务的结束。