这是我参与8月更文挑战的第3天,活动详情查看:8月更文挑战
在执行异步的时候,需要用到任务并行执行,在提到并行的时候,很多人都会想到并发的概念,这两者虽然只有一字之差,但是功能确有者天壤之别
并行
并行是指多个任务在同一时间点发生,并有不同的cou进行处理,不互相抢占资源。通俗易懂就是:每个任务都用不同的CPU来负责执行。
并发
并发是指多个任务在同一时间点给人感觉是同时执行,但是油同一个CPU不同的切换来执行,不同的任务之间互相抢夺CPU的资源。通俗易懂就是:多个任务抢夺同一个CPU资源,CPU不停的切换上下文来执行不同的任务。
当大量数据处理的时候,数据并行化可以大量缩短任务的执行时间,例如Java8中的Stream流,我们就可以采用parallelStream将并行流转换为串行流,将同一份数据分解成多个部分,然后并行处理,最后再将执行的结果汇总,得到最终的执行结果,极大的加快了执行的速率。 parallelStream流在底层实现的时候,采用的是fork/join分解合并框架来进行实现的。
并行流的使用,并不是并行的速度一定比串行快,并行在不同的情况下不一定比串行执行的快,影响并行流执行性能主要为下面的几个因素:
- 数据大小: 输入数据的大小,直接影响了并行处理的性能。因为在并行内部实现中涉及到了fork/join操作,它本身
就存在性能上的开销。因此只有当数据量很大,使用并行处理才有意义
- 源数据结构: fork时会对源数据进行分割,数据源的特性直接影响了fork的性能
- ArrayList、数组或IntStream.range,可分解性最佳, 因为他们都支持随机读取,因此可以被任意分割
- HashSet、TreeSet,可分解性一般,其虽然可被分解,但 因为其内部数据结构,很难被平均分解
- LinkedList、Streams.iterate、 BufferedReader.lines,可分解性极差,因为他们长度未 知,无法确定在哪里进行分割
- 装箱拆箱: 尽量使用基本数据类型,避免装箱拆箱
- CPU核数: fork的产生数量是与可用CPU核数相关,可用的核数越多,获取的性能提升就会越大
- 单元处理开销: 花在流中每个元素的时间越长,并行操作带来的性能提升就会越明显
Fork/Join框架
Fork/Join框架是Java7提供的一个用于并行执行任务的框架,把一个大任务分割成若干个小任务,等若干个子任务执行完成后,将子任务的结果汇总后得到大任务结果的框架。Fork Join的运行流程图如下:
从流程图可以看出,Fork/Join框架分为两个步骤:
- 分割任务: 将大任务分割成若干个子任务,有可能子任务还是很大,就需要不停的往下面分割,直到分割的任务很小才不往下分割。
- 执行任务并且合并结果: 分割的子任务分别放在双端队列里,然后启动几个线程分别从双端的队列获取任务并执行。子任务执行完成的结果都统一放在一个队列里,然后又会启动一个线程从结果队列里获取执行的结果,合并结果数据。
但是有个问题:双端的队列,由于数据的不同造成两边的执行效率不一致,有一端的队列任务执行速度很快,另一端的队列任务执行速度比较慢,则会拖慢整个任务的执行速度,为了解决这种问题,Fork/Join框架采用了一种工作窃取算法来解决这种弊端。何为工作窃取算法?看下面的描述:
工作窃取算法:
指某个线程自己的队列任务执行完成后,从其他的队列窃取任务来执行,由于不同的线程访问同一个队列,会造成多个线程获取队列任务的竞争,为了减少窃取任何和被窃取任务之间的竞争,通常会常用双端队列,被窃取任务线程从双端队列的头部拿任务执行,窃取任务的线程永远从双端队列的尾部拿任务执行,这样可以减少互相之间的竞争。
工作窃取算法的有点:这样可以充分利用线程资源并行计算,加快执行的效率,又由于采取了双端队列,减少了线程之间的竞争。但是在某些情况下,还是会存在竞争,当队列只有一个任务时,就会造成资源竞争,还有在执行多需要创建多个线程和多个双端队列,会加大资源的开销,这些都是工作窃取算法的缺点。