五、串并行流及Fork/Join框架
1.串行、并行和并发
- 串行:执行多个任务时,各个任务按照顺序执行,执行完一个才执行下一个。
- 并行:执行多个任务时,各个任务可以同时执行(多核CPU)。
- 并发:执行多个任务时,各个任务被不同线程执行(单核CPU),本质上是线程抢占到时间片后执行任务,并不是同时执行多个任务。
2.思考问题
当我们需要执行一个数据量庞大的任务时,我们可以将任务分割成许多较小的任务。串行就是将这些小任务逐个按顺序完成。并行就是将这些小任务按照某种标准分配给不同的CPU内核,然后每个CPU内核的不同线程去执行各自CPU所分配的任务。哪个效率会更高呢?显然并行的方式执行更优秀。
但是线程有可能发生阻塞,导致某个CPU内核的任务无法快速执行完成,而另一个CPU内核执行完任务空闲了下来,但是任务完成的整体效率却降低了,这该如何解决?试想如果空闲的CPU内核能将其他CPU内核的任务拿来执行,这样效率会不会就大大提高了呢!
上边提到的这种方式称为“工作窃取模式”:
当执行新的任务时它可以将其拆分分成更小的任务执行,并将小任务加到线程队列中,然后再从一个随机线程的队列中偷一个并把它放在自己的队列中。
Fork/Join 框架就是采用这种思想,下边我们来了解一下Fork/Join 框架。
3.Fork/Join 框架
Fork/Join 框架:就是在必要的情况下,将一个大任务,进行拆分(fork)成若干个小任务(拆到不可再拆时),再将一个个的小任务运算的结果进行 join 汇总.
相对于一般的线程池实现,fork/join框架的优势体现在对其中包含的任务的处理方式上.在一般的线程池中,如果一个线程正在执行的任务由于某些原因无法继续运行,那么该线程会处于等待状态.而在fork/join框架实现中,如果某个子问题由于等待另外一个子问题的完成而无法继续运行.那么处理该子问题的线程会主动寻找其他尚未运行的子问题来执行.这种方式减少了线程的等待时间,提高了性能 .
使用:
①继承RecursiveTask<>类,重写compute()方法(有返回值)
②继承RecursiveAction类,重写compute()方法(无返回值)
举个栗子:实现连续整数的累加
用for循环方式:
@Test
public void test2(){
long startTime = System.currentTimeMillis();
long sum = 0;
for (long i = 0; i<=100000000000L; i++) {
sum+=i;
}
System.out.println(sum);
long endTime = System.currentTimeMillis();
System.out.println("for循环方式执行时间为:"+(endTime-startTime)+"ms");
}
结果:
CPU利用率:
用Fork/Join方式:
package com.zsr.forkandjoin;
import java.util.concurrent.RecursiveTask;
/**
* 需求:大数的累加求和
*
* @author
* @date 2019/11/6 21:35
*/
public class Sum extends RecursiveTask<Long> {
private long begin;
private long end;
//进行拆分的临界值(标准)
private static final long THRESHOLD = 10000L;
public Sum(long begin, long end) {
this.begin = begin;
this.end = end;
}
@Override
protected Long compute() {
long length = end-begin;
//如果要计算的长度小于临界值,则不再拆分直接求和,否则继续拆分直至长度小于临界值
if (length<=THRESHOLD){
long sum = 0;
for (long i = begin;i <= end;i++){
sum += i;
}
return sum;
}else {
long mid = (begin+end)/2;
Sum left = new Sum(begin,mid);
left.fork();//拆分子任务,压入线程队列
Sum right = new Sum(mid+1,end);
right.fork();
return left.join()+right.join();
}
}
}
@Test
public void test1(){
long startTime = System.currentTimeMillis();
ForkJoinPool forkJoinPool = new ForkJoinPool();
ForkJoinTask task = new Sum(0,100000000000L);
Long result = (Long) forkJoinPool.invoke(task);
System.out.println(result);
long endTime = System.currentTimeMillis();
System.out.println("ForkJoin方式执行时间为:"+(endTime-startTime)+"ms");
}
结果:
CPU利用率:
显然Fork/Join方式的执行效率更高,同时有效的利用了CPU的资源。但是我们也发现Fork/Join方式相较与普通方式也更加繁琐,在Java1.8中得到了优化,让程序员用一种更加便捷的对数据进行并行的操作,下边进行介绍
↓↓↓↓
4.串并行流
串行流:将某个内容的流用串行的方式进行处理。
并行流:把某个内容分成多个数据块,并用不同的线程分别处理每个数据块的流。
Stream API 可以声明性地通过 parallel() 与sequential() 在并行流与顺序流之间进行切换。
int arr[] = {1,8,45,6,12,44};
//得到串行流
Arrays.stream(arr).sequential();
//得到并行流
Arrays.stream(arr).parallel();
依旧以“实现连续整数的累加”为例:
@Test
public void test3(){
long startTime = System.currentTimeMillis();
long sum = LongStream.rangeClosed(0, 100000000000L)
.parallel()
.reduce(Long::sum)
.getAsLong();
System.out.println(sum);
long endTime = System.currentTimeMillis();
System.out.println("f串行流方式执行时间为:"+(endTime-startTime)+"ms");
}
结果:
CPU利用率:
4.总结
Stream API为我们提供了方便的对流进行切换方法,在并行流的方式下,可以更加高效和方便的进行并行操作。由于并行流的底层采用Fork/Join的方式来进行并行操作,在数据量巨大的情况下提高了CPU的利用率,大大提升了程序的执行效率。
注:菜鸟一枚,才疏学浅,希望各位前辈能够批评指正,感谢感谢!!!