为了让我们的程序运行的更加高效,CPU的使用效率更高,我们可以通过让程序并行执行的方式让所有的CPU都忙碌起来,从而提供程序执行的效率。
有两种方式来实现并行:java8的fork-join框架、java8中的并行流(底层依然是fork-join框架)。
这里我们以计算n以内数字的和为例进行改进,也让我们能够很好的看到效果。
首先,我们定义要求和的最大数为:Long max = 1000000000L;
首先我们使用经典的for循环,串行进行遍历求和:
@Test
public void serial() {
long sum = 0;
long start = System.currentTimeMillis();
for (long i = 0; i <= max; i++) {
sum += i;
}
long end = System.currentTimeMillis();
System.out.println(String.format("for循环串行计算,sum:%d,总共耗时为:%d", sum, (end - start)));
}
效果如下:
for循环串行计算,sum:500000000500000000,总共耗时为:363
接下来,我们使用stream来遍历求和,代码如下:
@Test
public void java8Stream() {
long start = System.currentTimeMillis();
Long sum = Stream.iterate(1L, i -> i + 1)
.limit(max)
.reduce(0L, Long::sum);
long end = System.currentTimeMillis();
System.out.println(String.format("java8流式计算,sum:%d,总共耗时为:%d", sum, (end - start)));
}
效果如下:
java8流式计算,sum:500000000500000000,总共耗时为:11660
我们会发现,这种方式比for循环慢了很多,产生的原因主要如下:
我们可以通过把流转换成并行流来进行计算
@Test
public void java8Parallel() {
long start = System.currentTimeMillis();
long sum = Stream.iterate(1L, i -> i + 1)
.limit(max)
.parallel() //获取并行流
.reduce(0L, Long::sum);
long end = System.currentTimeMillis();
System.out.println(String.format("Java8并行流计算,sum:%d,总共耗时为:%d", sum, (end - start)));
}
在使用的时候,我们应当尽量避免包装类的转换,所以,我们可以使用LongStream
来获取数据,这样的话,就避免了不必要的拆箱和装箱。其他的场景下,我们也需要注意这一点。
@Test
public void java8ParallelWtihoutPackage() {
long start = System.currentTimeMillis();
long sum = LongStream.rangeClosed(0, max)
.parallel() //获取并行流
.sum();
long end = System.currentTimeMillis();
System.out.println(String.format("Java8并行流计算,去掉装箱拆箱,sum:%d,总共耗时为:%d", sum, (end - start)));
}
效果如下:
Java8并行流计算,去掉装箱拆箱,sum:500000000500000000,总共耗时为:252
并行流的获取
上面演示了串行到并行流的演进过程,接下来,我们给出常用的并行流获取方式:
parallel()
方法实现;sequential
配置并行流使用的线程池
并行流原理
并行流的Stream在内部分成了几块。因此可以对不同的块独立并行进行归纳操作。最后,同一个归纳操作会将各个子流的部分归纳结果合并起来,得到整个原始流的归纳结果
并行流使用原则
流的数据源和可分解性
需要注意的是:并行流的底层,依然采用的是fork-join框架。
分支/合并框架的目的是以递归方式将可以并行的任务拆分成更小的任务,然后将每个子任 务的结果合并起来生成整体结果。它是ExecutorService接口的一个实现,它把子任务分配给 线程池(称为ForkJoinPool)中的工作线程。
使用fork-join框架来实现并行的步骤如下:
要把任务提交到这个池,必须创建RecursiveTask的一个子类,其中R是并行化任务(以 及所有子任务)产生的结果类型,或者如果任务不返回结果,则是RecursiveAction类型(当 然它可能会更新其他非局部机构)。要定义RecursiveTask,只需实现它唯一的抽象方法 compute: protected abstract R compute();
这个方法同时定义了将任务拆分成子任务的逻辑,以及无法再拆分或不方便再拆分时,生成 单个子任务结果的逻辑。正由于此,这个方法的实现类似于下面的伪代码:
if (任务足够小或不可分) { 顺序计算该任务
} else {
将任务分成两个子任务
递归调用本方法,拆分每个子任务,等待所有子任务完成
合并每个子任务的结果
}
package com.firewolf.java8.s005.parallasync;
import java.util.concurrent.RecursiveTask;
/**
* Java7中的并行计算
* 定义一个用于拆分和合并的计算类
* 这个类需要继承RecursiveAction(没有返回值)或者是RecursiveTask(有返回值)
*
* @author liuxing
*/
public class ForkCalculater extends RecursiveTask {
private static final long serialVersionUID = -6790744108691400188L;
private long start;
private long end;
private long boundary = 10000;
public ForkCalculater(long start, long end) {
super();
this.start = start;
this.end = end;
}
@Override
protected Long compute() {
long length = end - start;
if (length >= boundary) { //进行任务划分
long middle = (start + end) / 2;
ForkCalculater left = new ForkCalculater(start, middle);
left.fork(); //利用另一个ForkJoinPool线程异步执行新创建的子任务
ForkCalculater right = new ForkCalculater(middle + 1, end);
Long rightResult = right.compute(); //同步执行右边的,这样可以减少提交到线程池中的任务,当然,调用join也是可以的
Long leftResult = left.join(); // 同步等在左边的结果
return leftResult + rightResult;
} else {// 不能再划分的时候,进行计算
long sum = 0;
for (long i = start; i <= end; i++) {
sum += i;
}
return sum;
}
}
}
@Test
public void fork_join() {
ForkJoinPool pool = new ForkJoinPool();
ForkJoinTask t = new ForkCalculater(0, max);
long start = System.currentTimeMillis();
Long sum = pool.invoke(t);
long end = System.currentTimeMillis();
System.out.println(String.format("fork-join计算框架,sum:%d,总共耗时为:%d", sum, (end - start)));
}
效果:
fork-join计算框架,sum:500000000500000000,总共耗时为:279
我们可以看到,效率也是非常的高
而问题在于,代码写起来太过麻烦,主要是RecursiveTask的编写,比较痛苦
fork-join采用了一种“工作窃取”的技术来提供计算的效率,具体如下:
理想情况下,划分并行任务时, 应该让每个任务都用完全相同的时间完成,让所有的CPU内核都同样繁忙。不幸的是,实际中,每 个子任务所花的时间可能天差地别,要么是因为划分策略效率低,要么是有不可预知的原因,比如 磁盘访问慢,或是需要和外部服务协调执行。
分支/合并框架工程用一种称为工作窃取(work stealing)的技术来解决这个问题。在实际应 用中,这意味着这些任务差不多被平均分配到ForkJoinPool中的所有线程上。每个线程都为分 配给它的任务保存一个双向链式队列,每完成一个任务,就会从队列头上取出下一个任务开始执 行。基于前面所述的原因,某个线程可能早早完成了分配给它的所有任务,也就是它的队列已经 空了,而其他的线程还很忙。这时,这个线程并没有闲下来,而是随机选了一个别的线程,从队 列的尾巴上“偷走”一个任务。这个过程一直继续下去,直到所有的任务都执行完毕,所有的队 列都清空。这就是为什么要划成许多小任务而不是少数几个大任务,这有助于更好地在工作线程 之间平衡负载。
Spliterator是Java 8中加入的另一个新接口;这个名字代表“可分迭代器”(splitable iterator)。和Iterator一样,Spliterator也用于遍历数据源中的元素,但它是为了并行执行 而设计的
Stream的并行计算,就是依赖了Spliterator来自动的对流进行了拆分。
通常情况下,我们不需要自己实现,当然如果需要实现的话,我们需要去实现Spliterator接口。
这个接口定义的几个方法如下:
boolean tryAdvance(Consumer super T> action);
:类似于普通的 Iterator,因为它会按顺序一个一个使用Spliterator中的元素,并且如果还有其他元素要遍 历就返回true
Spliterator
:专为Spliterator接口设计的,因为它可以把一些元素划出去分 给第二个Spliterator(由该方法返回),让它们两个并行处理,需要注意的是, 这里仅仅返回划分出来的那一部分。
long estimateSize();
:估计还剩下多少元素要遍历,因为即使不那么确切,能快速算出来是一个值 也有助于让拆分均匀一点
int characteristics();
:返回这个Spliterator的特性集合,可选值如下:
如果有多个特点,就加起来
将Stream拆分成多个部分的算法是一个递归过程。第一步是对第一个 Spliterator调用trySplit,生成第二个Spliterator。第二步对这两个Spliterator调用 trysplit,这样总共就有了四个Spliterator。这个框架不断对Spliterator调用trySplit 直到它返回null,表明它处理的数据结构不能再分割,如图所示:
这里以统计字符串中单词的数量来示例
字符串内容为:
private final String CONTENTS = "Nel mezzo del cammin di nostra vita i ritrovai in una selva oscura ché la dritta via era smarrita";
为了演示效果,这里没有使用字符串的方法。
@Test
public void forWordCounter() {
int counter = 0;
boolean lastSpace = true;
for (char c : CONTENTS.toCharArray()) {
if (Character.isWhitespace(c)) {
lastSpace = true;
} else {
if (lastSpace)
counter++;
lastSpace = false;
}
}
System.out.println(counter);
}
结果是19个
由于每个字符传入后,需要返回单词的数量已经是否是空格,所以需要顶一个对象来实现
package com.firewolf.java8.s005.parallasync;
/**
* 单词统计器
*/
public class WordCounter {
private int counter; //单词数量
private boolean isWhitespace; //是否是空格
public WordCounter(int counter, boolean isWhitespace) {
this.counter = counter;
this.isWhitespace = isWhitespace;
}
/**
* 累积函数,对每一个字符进行处理
* @param c 要被处理的字符
* @return
*/
public WordCounter accumulate(Character c) {
if (Character.isWhitespace(c)) { // 当前传入的字符为空
return new WordCounter(this.counter, true);
} else { // 当前传入的字符不为空,那么如果上一个字符为空,数量就要+1了,
return isWhitespace ? new WordCounter(this.counter + 1, false) : new WordCounter(this.counter, false);
}
}
/**
* 合并函数,把两个结果合并成一个结果
* @param wc 另外一个结果
* @return 合并后的结果
*/
public WordCounter combiner(WordCounter wc) {
return new WordCounter(wc.counter + this.counter, wc.isWhitespace);
}
/**
* 返回当前统计的单词数量
* @return 单词数量
*/
public int getCounter() {
return this.counter;
}
}
这里面还同时定义了累计函数和合并函数
接下来,进行计算
/**
* 通过流来统计单词个数
*/
@Test
public void streamWordCounter() {
Stream charStream = transStr2CharStream();
countWords(charStream);
}
/**
* 通过流统计单词数量
* @param stream
*/
private void countWords(Stream stream) {
WordCounter reduce = stream.reduce(new WordCounter(0, true), WordCounter::accumulate, WordCounter::combiner);
System.out.println(reduce.getCounter());
}
这个结果也没什么问题。
@Test
public void parallStramWordCounter(){
Stream charStream = transStr2CharStream();
countWords(charStream.parallel());
}
/**
* 把字符串转换成流
*
* @return
*/
private Stream transStr2CharStream() {
Stream charStream = IntStream.range(0, CONTENTS.length()).mapToObj(CONTENTS::charAt);
return charStream;
}
得到的结果为30,是错误的,原因是底层进行拆分的时候,把单词给拆开了,为了解决这个问题,我们需要定义自己的Spliterator
自定义Spliterator如下:
package com.firewolf.java8.s005.parallasync;
import java.util.Spliterator;
import java.util.function.Consumer;
public class WCSpliterator implements Spliterator {
private String str; // 要被处理的字符串
private int curentIndex = 0; // 当前处理的字符的下标
public WCSpliterator(String str) {
this.str = str;
}
/**
* 普通的迭代
*
* @param action
* @return
*/
@Override
public boolean tryAdvance(Consumer super Character> action) {
action.accept(str.charAt(curentIndex++));
return curentIndex < str.length();
}
/**
* 拆分出来的迭代器
*
* @return
*/
//注意,返回的是拆分出来的这一部分
@Override
public Spliterator trySplit() {
int currentLenght = str.length() - curentIndex;
//长度小于10之后不再拆分,直接顺序处理,所以返回null
if (currentLenght < 10) {
return null;
}
for (int splitPos = currentLenght / 2 + curentIndex; splitPos < str.length(); splitPos++) {
if (Character.isWhitespace(str.charAt(splitPos))) {
Spliterator spliterator = new WCSpliterator(str.substring(curentIndex, splitPos));
curentIndex = splitPos;
return spliterator;
}
}
return null;
}
//估算剩余长度
@Override
public long estimateSize() {
return str.length() - curentIndex;
}
/**
* 返回这个Spliterator的特点
* ORDERED:顺序的(也就是String中各个Character的次序)
* SIZED: estimatedSize方法的返回值是精确的
* SUBSIZED: trySplit方法创建的其他Spliterator也有确切大小
* NONNULL: String中不能有为null的Character
* IMMUTABLE:在解析String时不能再添加Character,因为String本身是一个不可变类
*
* @return
*/
@Override
public int characteristics() {
return ORDERED + SIZED + SUBSIZED + NONNULL + IMMUTABLE;
}
}
计算代码:
@Test
public void parallSteamWCBySelfSpliterater(){
Spliterator spliterator = new WCSpliterator(CONTENTS);
Stream stream = StreamSupport.stream(spliterator, true);
countWords(stream.parallel());
}
这次的计算结果,就正确了