Fork/Join框架在不同配置下的表现如何?

正如即将上映的星球大战那样,Java 8的并行流也是毁誉参半。并行流(Parallel Stream)的语法糖就像预告片里的新型光剑一样令人兴奋不已。现在Java中实现并发编程存在多种方式,我们希望了解这么做所带来的性能提升及风险是什么。从经过260多次测试之后拿到的数据来看,还是增加了不少新的见解的,这里我们想和大家分享一下。

ExecutorService vs. Fork/Join框架 vs. 并行流

在很久很久以前,在一个遥远的星球上。。好吧,其实我只是想说,在10年前,Java的并发还只能通过第三方库来实现。然后Java 5到来了,并引入了java.util.concurrent包,上面带有深深的Doug Lea的烙印。ExecutorService为我们提供了一种简单的操作线程池的方式。当然了,java.util.concurrent包也在不断完善,Java 7中还引入了基于ExecutorService线程池实现的Fork/Join框架。对很多开发人员来说,Fork/Join框架仍然显得非常神秘,因此Java 8的stream提供了一种更为方便地使用它的方法。我们来看下这几种方式有什么不同之处。

我们来通过两个任务来进行测试,一个是CPU密集型的,一个是IO密集型的,同样的功能,分别在4种场景下进行测试。不同实现中线程的数量也是一个非常重要的因素,因此这个也是我们测试的目标之一。测试机器共有8个核,因此我们分别使用4,8,16,32个线程来进行测试。对每个任务而言,我们还会测试下单线程的版本,不过这个在图中并没有标出来,因为它的时间要长得多。如果想了解这些测试用例是如何运行的,你可以看一下最后的基础库一节。我们开始吧。

给一段580万行6GB大小的文本建立索引

在本次测试中我们生成了一个超大的文本文件,并通过相同的方法来建立索引。我们来看下结果如何:

单线程执行时间:176,267毫秒,大约3分钟。 注意,上图是从20000毫秒开始的。

1. 线程过少会浪费CPU,而过多则会增加负载

从图中第一个容易注意到的就是柱状图的形状——光从这4个数据就能大概了解到各个实现的表现是怎样的了。8个线程到16个线程这里有所倾斜,这是因为某些线程阻塞在了文件IO这里,因此增加线程能更好地使用CPU资源。而当加到32个线程时,由于增加了额外的开销,性能又开始会变差。

2. 并行流表现最佳。与直接使用Fork/Join相比要快1秒左右

并行流所提供的可不止是语法糖(这里指的并不是lambda表达式),而且它的性能也比Fork/Join框架以及ExecutorService要更好。索引完6GB大小的文件只需要24.33秒。请相信Java,它的性能也能做到很好。

3. 但是。。并行流的表现也是最糟糕的:唯独它是超过了30秒的

并行流为什么会影响性能,这里也给你上了一课。这在本来就运行着多线程应用的机器上是有可能的。由于可用的线程本身就很少了,直接使用Fork/Join框架要比使用并行流更好一些——两者的结果相差5秒,大约是18%的性能损耗。

4. 如果涉及到IO操作的话,不要使用默认的线程池大小

测试中使用默认线程池大小(默认值是机器的CPU核数,在这里是8)的并行流,跟使用16个线程相比要慢上2秒。也就是说使用默认的池大小则要慢了7%。这是由于阻塞的IO线程导致的。由于有很多线程处于等待状态,因此引入更多的线程能够更好地利用CPU资源,当其它线程在等待调度时不至于让它们闲着。

如果改变并行流的默认的Fork/Join池的大小?你可以通过一个JVM参数来修改公用的Fork/Join线程池的大小:

-Djava.util.concurrent.ForkJoinPool.common.parallelism=16

  

(默认情况下,所有的Fork/Join任务都会共用同一个线程池,线程的数量等于CPU的核数。好处就是当线程空闲下来时可以收来处理其它任务。)

或者,你还可以用下这个小技巧,用一个自定义的Fork/Join池来运行并行流。它会覆盖掉默认的公用的Fork/Join池并让你能够使用自己配置好的线程池。手段有点卑劣。测试中我们使用的是公用的线程池。

5. 单线程的性能跟最快的结果相比要慢7.25倍

并发能够提升7.25倍的性能,考虑到机器是8核的,也就是说接近是8倍的提升!还差的那点应该是消耗在线程的开销上了。不仅如此,即便是测试中表现最差的并行版本,也就是4个线程的并行流实现(30.23秒),也比单线程的版本(176.27秒)要快5.8倍。

如果不考虑IO的话呢?比如判断某个数是否是素数

对这次测试而言,我们将去除掉IO的部分,来测试下判断一个大整数是否是素数要花多长时间。这个数有多大?19位,1,530,692,068,127,007,263,换句话说,一百五十三万零六百九十二兆零六百八十一亿两千万七千二百六十三。好吧,让我透透气先。我们也没有做任何的优化,而是直接运算到它的平方根,为此我们还检查了所有的偶数,尽管这个大数并不能被2整除,这只是为了让运算的时间更久一些。先剧透一下:这的确是一个素数。每个实现运算的次数也都是一样的。

下面是测试的结果:

单线程执行时间:118,127毫秒,大约2分钟 注意,上图是从20000毫秒开始的

1. 8个线程与16个线程相差不大

和IO测试中不同,这里并没有IO调用,因此8个线程和16个线程的差别并不大,Fork/Join的版本例外。由于它的反常表现,我们还多运行了好几组测试以确保得到的结果是正确的,但事实表明,结果仍是一样。希望你能在下方的评论一栏说一下你对这个的看法。

2. 不同实现的最好结果都很接近

我们看到,不同的实现版本最快的结果都是一样的,大约是28秒左右。不管实现的方法如何,结果都大同小异。但这并不意味着使用哪种方法都一样。请看下面这点。

3. 并行流的线程处理开销要优于其它实现

这点非常有意思。在本次测试中,我们发现,并行流的16个线程的再次胜出。不止如此,在这次测试中,不管线程数是多少,并行流的表现都是最好的。

4. 单线程的版本比最快的结果要慢4.2倍

除此之外,在运行计算密集型任务时,并行版本的优势要比带有IO的测试要减少了2倍。由于这是个CPU密集型的测试,这个结果倒也说得过去,不像前面那个测试中那样,减少CPU的等待IO的时间能获得额外的收益。

结论

之前我也建议过大家读一下源码,了解下何时应该使用并行流,并且在Java中进行并发编程时,不要武断地下结论。最好的检验方式就是在演示环境中多跑跑类似的测试用例。需要特别注意的因素包括你所运行的硬件环境 (以及测试的硬件环境),还有应用程序的总线程数。包括公用Fork/Join的线程池以及团队中其它开发人员所写的代码中包含的线程。在你编写自己的并发逻辑前,最好先检查下上述这些情况,对你的应用程序有一个整体的了解。

基础库

我们是在EC2的c3.2xlarge实例上运行的本次测试,它有8个vCPU核以及15GB的内存。vCPU是因为这里用到了超线程技术,因此实际上只有4个物理核,但每个核模拟成了两个。对操作系统的调度器而言,认为我们一共有8个核。为了尽可能的公平,每个实现都运行了10遍,并选择了第2次到第9次的平均运行时间。也就是一共运行了260次!处理时长也非常重要。我们所选择的任务的运行时间都会超过20秒,因此时间差异能很容易看出来,而不太受外部因素的影响。

最后

原始的测试结果在这里,代码放在Github上。欢迎进行修改,并告诉我们你的测试结果。如果发现了什么我们这里没有讲到的有意思的新的见解或者现象,欢迎告诉我们,我们很希望能把它们追加到本文中。

Java中不同的并发实现的性能比较的更多相关文章

  1. Java中的线程--并发库中的集合

    线程中的知识点基本都已经学完了,看看Java5并发库中提供的集合... 一.可堵塞队列 队列包含固定长度的队列和不固定长度的队列 ArrayBlockQueue中只有put()方法和take()方法才 ...

  2. java中常用的并发工具类

    · 1. 等待多线程完成的CountDownLatch 构造函数接收一个int类型的参数作为计数器,如果想等待N个点,就传入N.当调用CountDownLatch的countDown方法时,N就会减一 ...

  3. Java 中 ConcurrentHashMap 的并发度是什么?

    ConcurrentHashMap 把实际 map 划分成若干部分来实现它的可扩展性和线程安 全.这种划分是使用并发度获得的,它是 ConcurrentHashMap 类构造函数的一 个可选参数,默认 ...

  4. Java 面试宝典!并发编程 71 道题及答案全送上!

    金九银十跳槽季已经开始,作为 Java 开发者你开始刷面试题了吗?别急,我整理了71道并发相关的面试题,看这一文就够了! 1.在java中守护线程和本地线程区别? java中的线程分为两种:守护线程( ...

  5. java中的各种Queue

    java中的各种并发Queue可以归为以下的几种: ConcurrentLinkedQueue: 一个由链表结构组成的非阻塞队列 ArrayBlockingQueue :一个由数组结构组成的有界阻塞队 ...

  6. Java中“==”和 equals的区别

    “==”的作用: 判断两个变量栈内存中存储的值是否相等,如果相等返回true,如果不相等返回false. 有两种形式的比较需要用到比较运算符“==”,一是两个基本数据类型之间的比较,二是两个引用数据类 ...

  7. JAVA中关于并发的一些理解

    一,JAVA线程是如何实现的? 同步,涉及到多线程操作,那在JAVA中线程是如何实现的呢? 操作系统中讲到,线程的实现(线程模型)主要有三种方式: ①使用内核线程实现 ②使用用户线程实现 ③使用用户线 ...

  8. java编程思想-java中的并发(二)

    二.共享受限资源 有了并发就可以同时做多件事情了.但是,两个或多个线程彼此互相干涉的问题也就出现了.如果不防范这种冲突,就可能发生两个线程同时试图访问同一个银行账户,或向同一个打印机打印,改变同一个值 ...

  9. JAVA中怎么处理高并发的情况

    一.背景综述 并发就是可以使用多个线程或进程,同时处理(就是并发)不同的操作. 高并发的时候就是有很多用户在访问,导致系统数据不正确.糗事数据的现象.对于一些大型网站,比如门户网站,在面对大量用户访问 ...

随机推荐

  1. 一些Python的惯用法和小技巧:Pythonic

    Pythonic其实是个模糊的含义,没有确定的解释.网上也没有过多关于Pythonic的说明,我个人的理解是更加Python,更符合Python的行为习惯.本文主要是说明一些Python的惯用法和小技 ...

  2. python中re.findall()找到的结果替换

    正则表达式re模块中用findall查找到的是ascii码,所以当比对替换时也需要对应的ascii码才能匹配成功.以下程序是查找文件夹下文件名中含有男.女的文件,并将男替换成1,将女替换成2的程序 # ...

  3. UNITY 状态机 + SVN + 码云 下篇

    上篇说到自己写的一个FSM状态机,这篇写怎么把代码和码云联系在一起! 首先,我们应该知道为什么使用码云? 码云是开源中国社区2013年推出的基于 Git 的完全免费的代码托管服务,这个服务是基于 Gi ...

  4. php+redis window

    http://download.csdn.net/detail/qwfy326/6572443 wampserver2.2e-php5.3.13-httpd2.2.22-mysql5.5.24-x64

  5. Jexus V5.8.2 正式发布,强劲的高性能 web 服务器

    Jexus 是一款运行于 Linux 平台,以支持  ASP.NET.PHP 为特色的集高安全性和高性能为一体的 WEB 服务器和反向代理服务器.最新版 5.8.2 已经发布. 有如下更新: 1,新增 ...

  6. Java设计模式2:简单工厂模式

    简单工厂模式 简单工厂模式是类的创建模式,又叫做静态工厂方法模式.简单工厂模式由一个工厂对象决定生产出哪一种产品类的实例. 为什么要使用简单工厂模式 原因很简单:解耦. A对象如果要调用B对象,最简单 ...

  7. DDD领域驱动设计之聚合、实体、值对象

    关于具体需求,请看前面的博文:DDD领域驱动设计实践篇之如何提取模型,下面是具体的实体.聚合.值对象的代码,不想多说什么是实体.聚合等概念,相信理论的东西大家已经知晓了.本人对DDD表示好奇,没有在真 ...

  8. java中基本类型和包装类型实践经验

    至今,小菜用java快两年了,有些事,也该有个总结. 基本类型和包装类型的概念在本文不作赘述. 如果这两种类型直接使用,倒没什么值得讨论的,无非就是自动装箱拆箱,java可以让你感觉不到他们的存在,但 ...

  9. 利用IFormattable接口自动参数化Sql语句

    提要 string.Format("{0},{1}",a,b)的用法大家都不陌生了,在很多项目中都会发现很多sql语句存在这样拼接的问题,这种做法很多"懒"程序 ...

  10. SWF READER 破解日志。

    网上传闻swf reader是破解最厉害的神器,可以内存抓取+doSWF反编译.所以去官网下了一个: SWF_Reader_2.3 不出所料,demo版本没有反编译的功能.网上搜到一个哥们尝试了下: ...