parallelStream是什么
parallelStream其实就是一个并行执行的流.它通过默认的ForkJoinPool,可能提高你的多线程任务的速度
.
parallelStream的作用
Stream具有平行处理能力,处理的过程会分而治之,也就是将一个大任务切分成多个小任务,这表示每个任务都是一个操作,因此像以下的程式片段:
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9);
numbers.parallelStream()
.forEach(out::println);
你得到的展示顺序不一定会是1、2、3、4、5、6、7、8、9,而可能是任意的顺序,就forEach()这个操作來讲,如果平行处理时,希望最后顺序是按照原来Stream的数据顺序,那可以调用forEachOrdered()。例如:
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9);
numbers.parallelStream()
.forEachOrdered(out::println);
注意:如果forEachOrdered()中间有其他如filter()的中介操作,会试着平行化处理,然后最终forEachOrdered()会以原数据顺序处理,因此,使用forEachOrdered()这类的有序处理,可能会(或完全失去)失去平行化的一些优势,实际上中介操作亦有可能如此,例如sorted()方法。
parallelStream背后:ForkJoinPool
ForkJoinPool主要用来使用分治法(Divide-and-Conquer Algorithm)来解决问题。典型的应用比如快速排序算法。这里的要点在于,ForkJoinPool需要使用相对少的线程来处理大量的任务。比如要对1000万个数据进行排序,那么会将这个任务分割成两个500万的排序任务和一个针对这两组500万数据的合并任务。最后会设置一个阈值来规定当数据规模到多少时,停止这样的分割处理。比如,当元素的数量小于10时,会停止分割,转而使用插入排序对它们进行排序。
那么使用ThreadPoolExecutor或者ForkJoinPool,会有什么性能的差异呢
ForkJoinPool能够使用数量有限的线程来完成非常多的具有父子关系的任务,比如使用4个线程来完成超过200万个任务
hreadPoolExecutor中的Thread无法选择优先执行子任务,需要完成200万个具有父子关系的任务时,也需要200万个线程
工作窃取算法
forkjoin最核心的地方就是利用了现代硬件设备多核,在一个操作时候会有空闲的cpu,那么如何利用好这个空闲的cpu就成了提高性能的关键,而这里我们要提到的工作窃取(work-stealing)算法就是整个forkjion框架的核心理念,工作窃取(work-stealing)算法是指某个线程从其他队列里窃取任务来执行。
有的线程会先把自己队列里的任务干完,而其他线程对应的队列里还有任务等待处理。干完活的线程与其等着,不如去帮其他线程干活,于是它就去其他线程的队列里窃取一个任务来执行。而在这时它们会访问同一个队列,所以为了减少窃取任务线程和被窃取任务线程之间的竞争,通常会使用双端队列,被窃取任务线程永远从双端队列的头部拿任务执行,而窃取任务的线程永远从双端队列的尾部拿任务执行
工作窃取算法的优点是充分利用线程进行并行计算,并减少了线程间的竞争,其缺点是在某些情况下还是存在竞争,比如双端队列里只有一个任务时。并且消耗了更多的系统资源,比如创建多个线程和多个双端队列
forkjoin
ava 8为ForkJoinPool添加了一个通用线程池,这个线程池用来处理那些没有被显式提交到任何线程池的任务。它是ForkJoinPool类型上的一个静态元素,它拥有的默认线程数量等于运行计算机上的处理器数量。当调用Arrays类上添加的新方法时,自动并行化就会发生。比如用来排序一个数组的并行快速排序,用来对一个数组中的元素进行并行遍历。自动并行化也被运用在Java 8新添加的Stream API中。
列表中的元素的操作都会以并行的方式执行。forEach方法会为每个元素的计算操作创建一个任务,该任务会被前文中提到的ForkJoinPool中的通用线程池处理。以上的并行计算逻辑当然也可以使用ThreadPoolExecutor完成,但是就代码的可读性和代码量而言,使用ForkJoinPool明显更胜一筹。
对于ForkJoinPool通用线程池的线程数量,通常使用默认值就可以了,即运行时计算机的处理器数量。我这里提供了一个示例的代码让你了解jvm所使用的ForkJoinPool的线程数量, 你可以可以通过设置系统属性:-Djava.util.concurrent.ForkJoinPool.common.parallelism=N
它会将执行forEach本身的线程也作为线程池中的一个工作线程。因此,即使将ForkJoinPool的通用线程池的线程数量设置为1,实际上也会有2个工作线程。因此在使用forEach的时候,线程数为1的ForkJoinPool通用线程池和线程数为2的ThreadPoolExecutor是等价的。
当ForkJoinPool通用线程池实际需要4个工作线程时,可以将它设置成3,那么在运行时可用的工作线程就是4
当需要处理递归分治算法时,考虑使用ForkJoinPool。 2. 仔细设置不再进行任务划分的阈值,这个阈值对性能有影响。 3. Java 8中的一些特性会使用到ForkJoinPool中的通用线程池。在某些场合下,需要调整该线程池的默认的线程数量。
stream or parallelStream?
1. 是否需要并行? 2. 任务之间是否是独立的?是否会引起任何竞态条件? 3. 结果是否取决于任务的调用顺序?
问题1,在回答这个问题之前,你需要弄清楚你要解决的问题是什么,数据量有多大,计算的特点是什么?并不是所有的问题都适合使用并发程序来求解,比如当数据量不大时,顺序执行往往比并行执行更快。毕竟,准备线程池和其它相关资源也是需要时间的。但是,当任务涉及到I/O操作并且任务之间不互相依赖时,那么并行化就是一个不错的选择。通常而言,将这类程序并行化之后,执行速度会提升好几个等级。
对于问题2,如果任务之间是独立的,并且代码中不涉及到对同一个对象的某个状态或者某个变量的更新操作,那么就表明代码是可以被并行化的。
对于问题3,由于在并行环境中任务的执行顺序是不确定的,因此对于依赖于顺序的任务而言,并行化也许不能给出正确的结果
坑点
1.线程不安全
parallelStream()加入这行代码,就成了多线程,HaashMap线程不安全,会导致各种多线程问题.
线程操作的对象需要是一个线程安全的对象
例如:
1)
HashMap<String, Object> hashMap = new HashMap<>();
应为
ConcurrentHashMap<String, Object> concurrentHashMap = new ConcurrentHashMap<>();
示例:
ConcurrentHashMap<String, Object> concurrentHashMap = new ConcurrentHashMap<>();
numList.parallelStream().forEach(curr -> {
concurrentHashMap.put(curr);
});
2.公共commonPool导致程序卡顿
parallelStream()底层用的ForkJoinPool.commonPool();进行并行计算。代码中多个脚本同时用到parallelStream时,会共用线程池,一个脚本io慢,其他脚本都等等,用到的脚本越多,卡的时间越长。
现象:cpu和内存都正常的情况下,生产环境遇到一次,脚本卡住几个小时才执行完,线程堆栈分析发现在下面代码线程进入waiting状态,而且是偶发。
3.并行代码中Threadlocal失效
parallelStream()是一个隐式线程池,比如:读写分离的连接名称、request通用获取等等,并行代码块中都将失效。
4.parallelStream()正确用法
1)cpu密集型的运算
parallelStream底层使用和cpu核数一样多的ForkJoinPool并行计算,适合cpu密集型业务直接使用。
2)io密集的多线程场景
首先io密集场景适合使用线程池,不建议使用parallelStream()
如果非要使用,可以做如下优化:
io密集的业务,消耗cpu较少,出现慢sql等场景会导致线程等待,不能使用默认ForkJoinPool.commonPool(),自定义ForkJoinPool。
ForkJoinPool forkJoinPool = new ForkJoinPool();
forkJoinPool.execute(() -> {
aaaList.parallelStream().forEach(curr -> {
// 正常编写业务代码
});
});
注意事项总结
并行代码块中需要使用AtomicInteger、ConcurrentHashMap等线程安全类。
parallelStream默认使用的commonPool,在io密集场景下不可大量使用
数量级小的计算就别用并行了,cpu切换耗时反而慢
在用到threadlocal的情景下,谨慎使用parallelStream和线程池,多线程中无法获取主线程的threadlocal。
如果完全不懂parallelStream底层原理,建议不要使用
本文为原博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/zjy_love_java/article/details/108562433