并行流
将顺序流转换为并行流
package top.hengshare.interviewer.java8.stream;
import java.util.function.Function;
import java.util.stream.LongStream;
import java.util.stream.Stream;
/**
* @author Yang
*/
public class ParallelStream {
public static long sequentialSum(long n) {
return Stream.iterate(1L, i -> i + 1)
.limit(n)
.reduce(0L, Long::sum);
}
public static long iterativeSum(long n) {
long result = 0;
for (long i = 1L; i <= n; i++) {
result += i;
}
return result;
}
public static long parallelSum(long n) {
return Stream.iterate(1L, i -> i + 1)
.limit(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 long parallelRangedSum(long n) {
return LongStream.rangeClosed(1, n)
.parallel()
.reduce(0L, Long::sum);
}
public static long rangedSum(long n) {
return LongStream.rangeClosed(1, n)
.reduce(0L, Long::sum);
}
public static void main(String[] args) {
System.out.println("Sequential sum done in:" +
measureSumPerf(ParallelStream::sequentialSum, 10_000_000) + " msecs");
System.out.println("Iterative sum done in:" +
measureSumPerf(ParallelStream::iterativeSum, 10_000_000) + " msecs");
System.out.println("Parallel sum done in: " +
measureSumPerf(ParallelStream::parallelSum, 10_000_000) + " msecs" );
System.out.println("ranged Sum done in:" +
measureSumPerf(ParallelStream::rangedSum, 10_000_000) + " msecs");
System.out.println("Parallel range sum done in:" +
measureSumPerf(ParallelStream::parallelRangedSum, 10_000_000) + " msecs");
}
}
正确使用并行流
public static long sideEffectSum(long n) {
Accumulator accumulator = new Accumulator();
LongStream.rangeClosed(1, n).forEach(accumulator::add);
return accumulator.total;
}
public class Accumulator {
public long total = 0;
public void add(long value) { total += value; }
}
高效使用并行流
- 在使用并行流替代顺序流之前,应该先测试。
- 留意装箱和拆箱,使用原始类型流避免。
- 有些操作本身在并行流上的性能就比顺序流差。
- 使用N*Q来评判,N代表流水线,Q代表任务的处理时长。Q越大,优化的效果越好。
- 较小的数据量,并行流不会太适用。
- 要考虑背后的数据结构是否容易分解。
- 要考虑流自身的特点、以及流水线中间操作修改流的方式。
- 要考虑最后合并操作中的效率,效率太低,并行可能不会很适用。
源 可分解性
ArrayList 极佳
LinkedList 差
IntStream.range 极佳
Stream.iterate 差
HashSet 好
TreeSet 好
分支合并框架
使用Recursive Task
package top.hengshare.interviewer.java8.ForkAndJoin;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.ForkJoinTask;
import java.util.concurrent.RecursiveTask;
import java.util.stream.LongStream;
/**
* 分支合并框架代码
* @author Yang
*/
public class ForkJoinSumCalculator extends RecursiveTask {
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);
}
public 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();
}
//创建一个子任务来为数组的前一半求和
ForkJoinSumCalculator leftTask = new ForkJoinSumCalculator(numbers, start, start + length / 2);
//使用子任务的fork分支方法异步执行子任务
leftTask.fork();
//再创建一个子任务来为数组的后一半求和
ForkJoinSumCalculator rightTask = new ForkJoinSumCalculator(numbers, start + length / 2, end);
//同步执行第二个子任务,有可能允许进一步递归划分子任务
Long rightResult = rightTask.compute();
System.out.print(rightResult + "+");
//读取第一个子任务的结果,如果尚未完成则等待
Long leftResult = (Long) leftTask.join();
System.out.println(leftResult);
//该任务的结果是两个子任务的结构的和
return rightResult + leftResult;
}
/**
* 在子任务不可再划分的时候计算结果的简单算法
* @return 一个长整型的值
*/
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> calculator = new ForkJoinSumCalculator(numbers);
return new ForkJoinPool().invoke(calculator);
}
public static void main(String[] args) {
System.out.println("start:");
System.out.println("\n" + forkJoinSum(1_000_000));
System.out.println("\nend.");
}
}
使用分支合并框架的最佳做法
- 对一个任务调用join方法会阻塞调用方,直到该任务做出结果。因此,有必要在两个子任务的计算都开始之后再调用它。否则,你得到的版本会比原始的顺序算法更慢更复杂,因为每个子任务都必须等待另一个子任务完成才能启动。
- 不应该在RecursiveTask内部使用ForkJoinPool的invoke方法。相反,你应该始终直接调用compute或fork方法,只有顺序代码才应该用invoke来启动并行计算。
- 对子任务调用fork方法可以把它排进ForkJoinPool。同时对左边和右边的子任务调用它似乎很自然,但这样做的效率要比直接对其中一个调用compute低。这样做你可以为其中一个子任务重用同一线程,从而避免在线程池中多分配一个任务造成的开销。
- 调试使用分支/合并框架的并行计算可能有点棘手。特别是你平常都在你喜欢的IDE里面看栈跟踪( stack trace)来找问题,但放在分支合并计算上就不行了,因为调用compute的线程并不是概念上的调用方,后者是调用fork的那个。
- 和并行流一样,你不应理所当然地认为在多核处理器上使用分支/合并框架就比顺序计算快。我们已经说过,一个任务可以分解成多个独立的子任务,才能让性能在并行化时有所提升。所有这些子任务的运行时间都应该比分出新任务所花的时间长;一个惯用方法是把输入/输出放在一个子任务里,计算放在另一个里,这样计算就可以和输入/输出同时进行。此外,在比较同一算法的顺序和并行版本的性能时还有别的因素要考虑。就像任何其他Java代码一样,分支/合并框架需要“预热”或者说要执行几遍才会被JIT编译器优化。这就是为什么在测量性能之前跑几遍程序很重要,我们的测试框架就是这么做的。同时还要知道,编译器内置的优化可能会为顺序版本带来一些优势(例如执行死码分析——删去从未被使用的计算)。
工作窃取
工作窃取( work stealing)
每个线程都为分配给它的任务保存一个双向链式队列,每完成一个任务,就会从队列头上取出下一个任务开始执行。基于前面所述的原因,某个线程可能早早完成了分配给它的所有任务,也就是它的队列已经空了,而其他的线程还很忙。这时,这个线程并没有闲下来,而是随机选了一个别的线程,从队列的尾巴上“偷走”一个任务。这个过程一直继续下去,直到所有的任务都执行完毕,所有的队列都清空。这就是为什么要划成许多小任务而不是少数几个大任务,这有助于更好地在工作线程之间平衡负载。
spliterator
使用spliterator自动拆分流
拆分过程
将Stream拆分成多个部分的算法是一个递归过程,如图7-6所示。第一步是对第一个Spliterator调用trySplit,生成第二个Spliterator。第二步对这两个Spliterator调用trysplit,这样总共就有了四个Spliterator。这个框架不断对Spliterator调用trySplit直到它返回null,表明它处理的数据结构不能再分割,如第三步所示。最后,这个递归拆分过程到第四步就终止了,这时所有的Spliterator在调用trySplit时都返回了null。
实现自己的拆分器apliterator
package top.hengshare.interviewer.java8.ForkAndJoin;
import java.io.StringReader;
import java.util.AbstractMap;
import java.util.stream.IntStream;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;
public class MySpliterator {
private static final String SENTENCE =
" Nel mezzo del cammin di nostra vita " +
"mi ritrovai in una selva oscura" +
" ché la dritta via era smarrita ";
/**
* 普通风格的单词计数器实现
*
* @param s 字符串
* @return 单词个数
*/
public static int countWordsIteratively(String s) {
int counter = 0;
boolean lastSpace = true;
for (char c : s.toCharArray()) {
if (Character.isWhitespace(c)) {
lastSpace = true;
} else {
if (lastSpace) {
counter++;
}
lastSpace = false;
}
}
return counter;
}
/**
* 使用函数式风格对字符串中单词个数进行统计
* @param stream
* @return
*/
private static int countWords(Stream<Character> stream) {
WordCounter counter = stream.reduce(new WordCounter(0, true),
WordCounter::accumulate,
WordCounter::combine);
return counter.getCounter();
}
public static void main(String[] args) {
//普通风格的字符串统计
System.out.println(countWordsIteratively(SENTENCE));
//串行函数式风格字符串统计
Stream<Character> stream = IntStream.range(0, SENTENCE.length())
.mapToObj(SENTENCE::charAt);
System.out.println(countWords(stream));
//错误的并行函数式风格字符串统计
Stream<Character> stream2 = IntStream.range(0, SENTENCE.length())
.mapToObj(SENTENCE::charAt);
System.out.println(countWords(stream2.parallel()));
//正确使用spliterator拆分数据源,制造并行流,完成字符串单词统计功能
WordCounterSpliterator wordCounterSpliterator = new WordCounterSpliterator(SENTENCE);
Stream<Character> stream3 = StreamSupport.stream(wordCounterSpliterator, true);
System.out.println(countWords(stream3));
}
}
class WordCounter {
private final int counter;
private final boolean lastSpace;
public WordCounter(int counter, boolean lastSpace) {
this.counter = counter;
this.lastSpace = lastSpace;
}
public WordCounter accumulate(Character c) {
if (Character.isWhitespace(c)) {
return lastSpace ? this : new WordCounter(counter, true);
} else {
//上一个字符是空格,而当前遍历的字符不是空格时,将单词计数器加一
return lastSpace ? new WordCounter(counter + 1, false) : this;
}
}
public WordCounter combine(WordCounter wordCounter) {
return new WordCounter(counter + wordCounter.counter, wordCounter.lastSpace);
}
public int getCounter() {
return counter;
}
}
class WordCounterSpliterator implements Spliterator<Character> {
private final String string;
private int currentChar = 0;
public WordCounterSpliterator(String string) {
this.string = string;
}
/**
* 判断是否还有字符要处理,如果有,返回true,否则返回false
*
* @param action action,用来定义要执行的动作
* @return 有:true;没有:false
*/
@Override
public boolean tryAdvance(Consumer<? super Character> action) {
action.accept(string.charAt(currentChar++));
return currentChar < string.length();
}
@Override
public Spliterator<Character> trySplit() {
int currentSize = string.length() - currentChar;
if (currentSize < 10) {
return null;
}
for (int spliPos = currentSize / 2 + currentChar; spliPos < string.length(); spliPos++) {
if (Character.isWhitespace(string.charAt(spliPos))) {
WordCounterSpliterator wordCounterSpliterator = new WordCounterSpliterator(string.substring(currentChar, spliPos));
currentChar = spliPos;
return wordCounterSpliterator;
}
}
return null;
}
@Override
public long estimateSize() {
return string.length() - currentChar;
}
@Override
public int characteristics() {
return ORDERED + SIZED + SUBSIZED + NONNULL + IMMUTABLE;
}
}
小结
- 内部迭代让你可以并行处理一个流,而无需在代码中显式使用和协调不同的线程。
- 虽然并行处理一个流很容易,却不能保证程序在所有情况下都运行得更快。并行软件的行为和性能有时是违反直觉的,因此一定要测量,确保你并没有把程序拖得更慢。
- 像并行流那样对一个数据集并行执行操作可以提升性能,特别是要处理的元素数量庞大,或处理单个元素特别耗时的时候。
- 从性能角度来看,使用正确的数据结构,如尽可能利用原始流而不是一般化的流,几乎总是比尝试并行化某些操作更为重要。
- 从性能角度来看,使用正确的数据结构,如尽可能利用原始流而不是一般化的流,几乎总是比尝试并行化某些操作更为重要。
- Spliterator定义了并行流如何拆分它要遍历的数据。