深入了解Java语言中的并发性选项有何不同
前言
Java™工程师在努力让并发性容易为开发人员所用。尽管做了不少的改进,但并发性仍然是Java平台的一个复杂、容易出错的部分。一部分复杂之处在于理解语言本身中的并发性的低级抽象,这些抽象在您的代码中填满了同步的代码块。另一个复杂之处来自一些新库,比如fork/join,这些库在某些场景中非常有用,但在其他场景中收效甚微。了解容易混乱的大量低级选项需要专业经验和时间。
脱离Java语言的优势之一是,能够改善和简化并发性等区域。每种Java下一代语言都为此问题提供了独特的答案,利用了该语言的默认编程风格。在本期文章中,我首先将会介绍函数式编程风格的优势:轻松并行化。我会深入分析Scala和Groovy的细节(下一期文章将全面介绍Clojure)。然后介绍Scalaactor。
完美数
数学家尼科马库斯(诞生于公元前6世纪)将自然数分为惟一的完美数(perfectnumber)、过剩数(abundantnumber)或亏数(deficientnumber)。一个完美数等于它的正因数(不包括它本身)之和。例如,6是一个完美数,因为它的因数是1、2、3和6,28也是完美数(28=1+2+4+7+14)。过剩数的因素之和大于该数,亏数的因数之和小于该数。
这里使用完美数分类法是为了方便介绍。除非要处理大量数字,是否查找因素对于从并行化中获益而言是一个微不足道的问题。使用更多线程可带来一些益处,但线程之间的切换开销对细粒度的作业而言代价很高。
让现有代码并行化
在“函数式编码风格”那一期的文章中,我们鼓励您使用更高级的抽象,比如化简、映射和过滤器,而不是迭代。此方法的优势之一是容易并行化。
我的函数式思维系列的读者熟悉包含完美数的数字分类模式(参见完美数边栏)。我在该系列中展示的任何解决方案都没有利用并发性。但是因为这些解决方案使用了转换函数,比如map,所以我可以在每种Java.net语言中做极少的工作来创建并行化的版本。
清单1是完美数分类器的一个Scala示例。
清单1.Scala中的并行完美数分类器
objectNumberClassifier{ defisFactor(factor:Int,number:Int)= number%factor==0 deffactors(number:Int)={ valfactorsBelowSqrt=(1toMath.sqrt(number).toInt).par.filter(isFactor(_,number)) valfactorsAboveSqrt=factorsBelowSqrt.par.map(number/_) (factorsBelowSqrt++factorsAboveSqrt).toList.distinct } defsum(factors:Seq[Int])= factors.par.foldLeft(0)(_+_) defisPerfect(number:Int)= sum(factors(number))-number==number }
清单1中的factors()方法返回一个数的因数列表,使用isFactor()方法过滤所有可能的值。factors()方法使用了我在“函数式思维:转换和优化”中更详细地介绍的一种优化。简单来讲,过滤每个数来查找因素的效率很低,因为根据定义,一个因数是其乘积等于目标数的两个数之一。
相反,我仅过滤不超过目标数的平方根的数,然后通过将目标数除以每个小于平方根的因数来生成对称因数列表。在清单1中,factorsBelowSqrt变量包含过滤操作的结果。factorsAboveSqrt的值是现有列表的映射,用于生成这些对称值。最后,factors()的返回值是一个串联的列表,它从一个并行的List转换为常规的List。
请注意,清单1中添加了par修饰符。该修饰符会导致filter、map和foldLeft并行运行,从而能够使用多个线程来处理请求。par方法(在整个Scala集合库中都是一致的)将该序列转换为并行序列。因为两种类型的序列反映了它们的签名,所以par函数变成了并行化某个操作的临时方式。
在Scala中并行化常见问题的简单性,在语言设计和函数模式上都经过证实。函数式编程鼓励使用通用的函数,比如map、filter和reduce,运行时以不可见的方式可以进一步优化它们。Scala语言设计人员考虑到了这些优化,最终产生了集合API的设计。
边缘情况
在清单1的factors()方法实现中,整数的平方根(例如,16的平方根:4)显示在两个列表中。因此,factors()方法返回的最后一行是对distinct函数的调用,它从列表中删除了重复值。您也可以在每一处都使用Set,而不是只在列表中使用它,但List常常拥有Set中所没有的有用函数。
Groovy也允许轻松地修改现有的函数代码,通过GPars库让它并行化,该库捆绑在各个Groovy发行版中。GPars框架在内置的Java并行性原语之上创建有用的抽象,常常将它们包装在语法糖中。GPars提供了令人眼花缭乱的并行机制,其中一种机制可用于分配线程池,然后将操作分布到这些池中。清单2中给出了一个使用Groovy编写的,使用GPars线程池的完美数分类器。
清单2.Groovy中的并行完美数分类器
classNumberClassifierPar{ staticdeffactors(number){ GParsPool.withPool{ deffactors=(1..round(sqrt(number)+1)).findAllParallel{number%it==0} (factors+factors.collectParallel{number/it}).unique() } } staticdefsumFactors(number){ factors(number).inject(0,{i,j->i+j}) } staticdefisPerfect(number){ sumFactors(number)-number==number } }
清单2中的factors()方法使用了与清单1相同的算法:它生成不超过目标数的平方根的所有因数,然后生成剩余的因数并返回串联的集合。与清单1中一样,我使用unique()方法来确保整数的平方根不会生成重复值。
无需像Scala中一样放大集合来创建对称并行版本,Groovy的设计人员创建了该语言的转换方法的xxxParallel()版本(例如findAllParallel()和collectParallel())。但除非这些方法包装在GPars线程池代码块中,否则它们不会起作用。
在清单2中,我创建了一个线程池,调用GParsPool.withPool创建一个代码块,支持在该代码块中使用xxxParallel()方法。withPool方法存在其他变体。例如,您可指定池中的线程数量。
Clojure通过化简器库提供了一种类似的临时并行化机制。使用转换函数的化简器版本来实现自动并行化,例如,
使用r/map代替map。(r/是化简器命名空间。)化简器的实现是Clojure的语法灵活性中的一个引人注目的案例分析,它通过极小的更改实现了强大的添加功能。
Scala中的actor
Scala包含众多并发性和并行性机制。一种较流行的机制是actor模型,它提供了将工作分布到线程上的优势,而没有同步的复杂性。在概念上,actor有能力完成工作,然后将一个非阻塞的结果发送给协调器。要创建一个actor,需要创建Actor类的子类并实现act()方法。通过使用Scala的语法糖,可绕过许多定义仪式,在代码块内定义actor。
我没有为清单1中的数字分类器执行的一种优化是,使用线程对作业的因数查找部分进行分区。如果我的计算机上有4个处理器,我可为每个处理器创建一个线程并拆分工作。例如,如果我尝试找到数字16的因数之和,那么我可以安排处理器1来查找从1到4的因数(并求和),安排处理器2来处理5到8,依此类推。使用actor是一种自然的选择:我为每个范围创建了一个actor,独立地执行每个actor(通过语法糖隐式执行或通过调用它的act()方法来显式执行),然后收集结果,如清单3所示。
清单3.使用Scala中的actor识别完美数
objectNumberClassifierextendsApp{ defisPerfect(candidate:Int)={ valRANGE=10000 valnumberOFPartitions=(candidate.toDouble/RANGE).ceil.toInt valcoordinator=self for(i<-0untilnumberOFPartitions){ vallower=i*RANGE+1 valupper=candidate.min((i+1)*RANGE) actor{ varpartialSum=0 for(j<-lowertoupper) if(candidate%j==0)partialSum+=j coordinator!partialSum } } varresponsesExpected=numberOFPartitions varsum=0 while(responsesExpected>0){ receive{ casepartialSum:Int=> responsesExpected-=1 sum+=partialSum } } sum==2*candidate } }
为了保持此示例的简单性,我将isPerfect()编写为单个完整的函数。我首先基于常量RANGE创建了一些分区。其次,我需要一种方式来收集actor所生成的消息。在coordinator变量中,我有一个引用可供actor向其发送消息,其中self是Actor的一个成员,表示Scala中获取线程标识符的可靠方式。
我然后为分区编号创建一个循环,使用RANGE偏移来生成范围的下限和上限。接下来,为该范围创建一个actor,使用Scala的语法糖来避免正式的类定义。在actor内,我为partialSum创建了一个临时保存器,然后分析该范围,将找到的因数收集到partialSum中。收集部分和(此范围内的所有因数的和)后,(coordinator!partialSum)向协调器发回一条消息,使用感叹号运算符。(这种消息传递语法的灵感来源于Erlang语言,用作一种对另一个线程执行非阻塞调用的途径。)
接下来,我启动了一个循环,等待所有actor完成处理。在等待过程中,我进入了一个receive代码块。在该代码块内,我想要一条Int消息,我在本地将它分配给partialSum,然后递减想要的响应数量,将该部分添加到总和中。所有actor完成且报告结果后,该方法的最后一行将该和与候选数的2倍相比较。如果比较结果为true,那么我的候选数就是一个完美数,该函数的返回值为true。
actor的一个不错的优势是所有权分区。每个actor都有一个partialSum局部变量,但它们从不彼此联系。通过协调器收到消息时,底层执行机制是不可见的:您创建了一个receive块,其他实现细节是不可见的。
Scala中的actor机制是Java下一代语言封装JVM的现有工具并使用一致的抽象来扩展它们的优秀示例。用Java语言编写类似的代码,并使用低级并发性原语,这些操作都需要非常复杂地协调多个线程。Scala中的actor隐藏了所有复杂性,留下的是容易理解的抽象。
结束语
Java下一代语言都为Java语言中的并发性难题提供了答案,而且每种语言以不同方式解决了这些问题。在本期文章中,我演示了所有三种Java下一代语言如何实现临时并行化。我还演示了Scala中的actor模型,构建了一个数字分类器来并行计算因数之和。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持毛票票。