并行流的考虑
并行流需要根据实际场景去应用,本身是有资源损耗的,在不同内核之间移动数据的代价是挺大的,一些普通的场景,比如单纯在几百个数中计算总和未必就比for循环高效。通过流来运算不免有些装箱拆箱的操作,若是比起for循环基本类型的运算就增多了这些损耗了。
public static long rangFor(long n){
return LongStream.rangeClosed(1, n)
.reduce(0L, Long::sum);
}
public static long rangParalle(long n){
return LongStream.rangeClosed(1, n)
.parallel()
.reduce(0L, Long::sum);
}
public static long measureSumPerf(Function<Long, Long> adder, long n) {
long fastest = Long.MAX_VALUE;
for (int i = 0; i < 10; i++) {
long start = System.nanoTime();
long sum = adder.apply(n);
long duration = (System.nanoTime() - start) / 1_000_000;
System.out.println("Result: " + sum);
if (duration < fastest) fastest = duration;
}
return fastest;
}
public static void main(String args[]){
System.out.println("for range sum done in:" +
measureSumPerf(ParalleMain::rangFor, 10_000_000) +
" msecs");
System.out.println("Parallel range sum done in:" +
measureSumPerf(ParalleMain::rangParalle, 10_000_000) +
" msecs");
}
并行流原理:分支/合并框架(ForkJoinPool)
分支/合并框架的目的是以递归方式将可以并行的任务?分成更小的任务,然后将每个子任
务的结果合并起来生成整体结果(拆分fork --》 执行 --》 合并join)
public class ForkJoinSumCalculator extends java.util.concurrent.RecursiveTask<Long> {
private final long[] numbers;
private final int start;
private final int end;
public static final long THRESHOLD = 10_000;
public ForkJoinSumCalculator(long[] numbers) {
this(numbers, 0, numbers.length);
}
private ForkJoinSumCalculator(long[] numbers, int start, int end) {
this.numbers = numbers;
this.start = start;
this.end = end;
}
@Override
protected Long compute() {
int length = end - start;
// 超过阈值则顺序流执行:分解任务到可顺序流执行的大小
if (length <= THRESHOLD) {
return computeSequentially();
}
// 切割一半任务到leftTask
ForkJoinSumCalculator leftTask =
new ForkJoinSumCalculator(numbers, start, start + length/2);
leftTask.fork();
ForkJoinSumCalculator rightTask =
new ForkJoinSumCalculator(numbers, start + length/2, end);
Long rightResult = rightTask.compute();
// 加入
Long leftResult = leftTask.join();
return leftResult + rightResult;
}
private long computeSequentially() {
long sum = 0;
for (int i = start; i < end; i++) {
sum += numbers[i];
}
return sum;
}
public static long forkJoinSum(long n) {
long[] numbers = LongStream.rangeClosed(1, n).toArray();
ForkJoinTask<Long> task = new ForkJoinSumCalculator(numbers);
return new ForkJoinPool().invoke(task);
}
}
工作窃取
上面的拆分任务代码虽然能起到并行拆分的作用,但若是输入量大,拆分的任务很多,可能成百上千个子任务,并行执行几百个任务,cpu核却只有四个的话,同样也是一种资源竞争了。
工作窃取:智能切割任务,任务组成队列形式,让任务平均分配到 ForkJoinPool 中的所有线程上,每个线程都为分配给它的任务保存一个双向链式队列,每完成一个任务,就会从队列头上取出下一个任务开始执行,某个线程可能早早完成了分配给它的所有任务这个线程并没有闲下来,而是随机选了一个别的线程,从队列的尾巴上“偷走”一个任务。
测试预热
分支/合并框架需要预热或者说要执行几遍才会被JIT编译器优化。这就是为什么在测量性能之前?几遍程序很重要,才能得到比较准备的数据。