并行处理与性能

1.Java 7之前并行处理数据

在Java 7之前,并行处理数据集合非常麻烦:

  • 你得明确地把包含数据的数据结构分成若干子部分
  • 你要给每个子部分分配一个独立的线程
  • 你需要在恰当的时候对它们进行同步来避免不希望出现的竞争条件,等待所有线程完成
  • 最后把这些部分结果合并起

2.并行流

Stream接口可以让你非常方便地处理它的元素,可以通过对收集源调用parallelStream或parallel方法来把集合转换为并行流。并行流就是一个把内容分成多个数据块,并用不同的线程分别处理每个数据块的流。这样一来,你就可以自动把给定操作的工作负荷分配给多核处理器的所有内核,让它们都忙起来。

2.1 parallel和sequential

  • 对顺序流调用parallel方法并不意味着流本身有任何实际的变化。它
    在内部实际上就是设了一个boolean标志,表示你想让调用parallel之后进行的所有操作都并
    行执行。

  • 类似地,你只需要对并行流调用sequential方法就可以把它变成顺序流。

  • 请注意,你可能以为把这两个方法结合起来,就可以更细化地控制在遍历流时哪些操作要并行执行,哪些要顺序执行。例如,你可以这样做:

    stream.parallel() 
     .filter(...) 
     .sequential() 
     .map(...) 
     .parallel() 
     .reduce(); 
    

    但最后一次parallel或sequential调用会影响整个流水线。在本例中,流水线会并行执
    行(而不是先串行,再并行),因为最后调用的是它。

2.2 正确使用并行流

  • 并行化并不是没有代价的。并行化过程本身需要对流做递归划分,把每个子流的归纳操作分配到不同的线程,然后把这些操作的结果合并成一个值。但在多个内核之间移动数据的代价也可能比你想的要大,所以很重要的一点是要保证在内核中并行执行工作的时间比在内核之间传输数据的时间长。
  • 避免共享可变状态,确保并行Stream得到正确的结果。

2.3 高效使用并行流

  • 如果有疑问,测量
  • 留意装箱
  • 有些操作本身在并行流上的性能就比顺序流差。
    特别是limit和findFirst等依赖于元素顺序的操作,它们在并行流上执行的代价非常大。例如,findAny会比findFirst性能好,因为它不一定要按顺序来执行。
  • 还要考虑流的操作流水线的总计算成本
  • 对于较小的数据量,选择并行流几乎从来都不是一个好的决定
  • 要考虑流背后的数据结构是否易于分解
    例如,ArrayList的拆分效率比LinkedList高得多,因为前者用不着遍历就可以平均拆分,而后者则必须遍历
  • 考虑终端操作中合并步骤的代价是大是小
    在这里插入图片描述

3.分支/合并框架

  • 分支/合并框架的目的是以递归方式将可以并行的任务拆分成更小的任务,然后将每个子任
    务的结果合并起来生成整体结果。
  • 它是ExecutorService接口的一个实现,它把子任务分配给线程池(称为ForkJoinPool)中的工作线程。

3.1 使用 RecursiveTask

创建ForkJoinPool实例后,可以ForkJoinPool的submit(ForkJoinTask task)或者invoke(ForkJoinTask task)来执行指定任务。其中ForkJoinTask代表一个可以并行、合并的任务。ForkJoinTask是一个抽象类,它有两个抽象子类:RecursiveAction和RecursiveTask。

  • RecursiveTask代表有返回值的任务
  • RecursiveAction代表没有返回值的任务

3.2 工作窃取

理想情况下,划分并行任务时,应该让每个任务都用完全相同的时间完成,让所有的CPU内核都同样繁忙。不幸的是,实际中,每个子任务所花的时间可能天差地别,要么是因为划分策略效率低,要么是有不可预知的原因,比如磁盘访问慢,或是需要和外部服务协调执行。分支/合并框架用一种称为工作窃取(work stealing)的技术来解决这个问题。

  • 工作窃取算法用于在池中的工作线程之间重新分配和平衡任务。
  • 每个线程都为分配给它的任务保存一个双向链式队列,每完成一个任务,就会从队列头上取出下一个任务开始执行 。基于前面所述的原因,某个线程可能早早完成了分配给它的所有任务,也就是它的队列已经空了,而其他的线程还很忙。这时,这个线程并没有闲下来,而是随机选了一个别的线程,从队列的尾巴上“偷走”一个任务。这个过程一直继续下去,直到所有的任务都执行完毕,所有的队列都清空。

3.3 Spliterator

Stream的并行执行是基本Spliterator接口,了解一下它的实现方式会让你对并行流的工作原理有更深入的了解。

public interface Spliterator<T> {
    
     
 boolean tryAdvance(Consumer<? super T> action);  //按顺序一个一个使用Spliterator中的元素
 Spliterator<T> trySplit();  //任务拆分,让它们两个并行处理
 long estimateSize();        //估计还剩下多少元素要遍历
 int characteristics();      //指定本身的特性,以便更好地控制和优化它的使用
}

猜你喜欢

转载自blog.csdn.net/weixin_42868638/article/details/110426369