fork join,fpga中always语句什么时候执行
fork join,fpga中always语句什么时候执行详细介绍
本文目录一览: ForkJoin框架的理解和使用
简单的说,Fork/Join是一个并行任务执行框架,能够把一个大的任务拆分成若干个小任务,并行地进行执行,最终还可以汇总各个小任务的执行结果。比如我们想计算1+2+...+100的结果,我们可以把这个大的任务拆分为10个小的任务,这10个小任务分别是1+...+10、11+...+20、...91+...+100,然后最终把这10个小任务的结果再加起来得到大任务的结果。
工作窃取算法,是指大任务被分成多个小任务的时候,这些小任务会被分到不通的任务队列中,每个任务队列会有一个工作线程来执行任务。但是在有些时候,有的线程会执行的比较快,提前完成了所有任务的执行,那么它就会去别的线程的任务队列中窃取任务进行执行,从而加快整体任务的完成。
但是窃取线程如何保证不和被窃取任务的线程冲突呢?这里用到了 双端队列 ,即任务队列都是这种数据结构,队列绑定的工作线程都从队列头部取任务进行执行,而窃取线程会从别的队列尾部获取任务进行执行。
工作窃取算法充分利用多线程进行并行计算,提高了执行效率,同时使用双端队列减少了线程间的冲突竞争;然后,不能完全避免冲突,比如某个任务队列中仅有一个任务的时候,两个线程同时竞争。还有,该算法会创建多个线程和多个双端队列,对系统资源的消耗会增加。
在使用之前,我们需要记住两个概念:
我们在compute中定义任务的拆分粒度和最小任务的执行逻辑,并通过fork和join能力来实现多线程并发。当子任务调用fork的时候,会继续执行子任务的compute;当子任务调用join的时候,会等待其所有子孙任务的执行结果。
并发(行)之ForkJoin
严格来说,Fork/join是并行而非并发的。之所以放到并发这块,是因为并发和并行大部分情况下是不需要程序员去关心的。大牛Linus Torvalds更是曾经讳言:
我们还能说什么呢?深以为然。 回到主题,Fork/join采用了分治思想,其实分治对我们来说很熟悉了,大学算法课程总会接触到,比如分治排序:
基于这个思想,我们先看下Fork/join的几个重要类。
另外还有个比较重要的变量 runState : 用来标记当前线程池的运行状态,使用二进制表示。 这就是值得我们学习借鉴的地方
还有重要的几个方法,我们用示例来说明吧,如下为求和示例:
结果如下:
我们看看程序中几个重要的方法: 1、fork()
2、join()
3、invoke()
4、invokeAll()
以上。
希望还有几天就到预产期的我媳妇和尚未出世的儿子都能健健康康。
ForkjoinPool -1
ForkJoin是用于并行执行任务的框架, 是一个把大任务分割成若干个小任务,最终汇总每个小任务结果后得到大任务结果的框架。Fork就是把一个大任务切分为若干子任务并行的执行,Join就是合并这些子任务的执行结果,最后得到这个大任务的结果。
下面是一个是一个简单的Join/Fork计算过程,将1—1001数字相加
通常这样个模型,你们会想到什么?
Release Framework ? 常见的处理模型是什么? task pool - worker pool的模型。 但是Forkjoinpool 采取了完全不同的模型。 ForkJoinPool一种ExecutorService的实现,运行ForkJoinTask任务。ForkJoinPool区别于其它ExecutorService,主要是因为它采用了一种工作窃取(work-stealing)的机制。所有被ForkJoinPool管理的线程尝试窃取提交到池子里的任务来执行,执行中又可产生子任务提交到池子中。 ForkJoinPool维护了一个WorkQueue的数组(数组长度是2的整数次方,自动增长)。每个workQueue都有任务队列(ForkJoinTask的数组),并且用base、top指向任务队列队尾和队头。work-stealing机制就是工作线程挨个扫描任务队列,如果队列不为空则取队尾的任务并执行。示意图如下
流程图:
pool属性
workQueues是pool的属性,它是WorkQueue类型的数组。externalPush和externalSubmit所创建的workQueue没有owner(即不是worker),且会被放到workQueues的偶数位置;而createWorker创建的workQueue(即worker)有owner,且会被放到workQueues的奇数位置。
WorkQueue的几个重要成员变量说明如下:
这是WorkQueue的config,高16位跟pool的config值保持一致,而低16位则是workQueue在workQueues数组的位置。 从workQueues属性的介绍中,我们知道,不是所有workQueue都有worker,没有worker的workQueue称为公共队列(shared queue),config的第32位就是用来判断是否是公共队列的。在externalSubmit创建工作队列时,有: q.config = k | SHARED_QUEUE; 其中q是新创建的workQueue,k就是q在workQueues数组中的位置,SHARED_QUEUE=1<<31,注意这里config没有保留mode的信息。 而在registerWorker中,则是这样给workQueue的config赋值的: w.config = i | mode; w是新创建的workQueue,i是其在workQueues数组中的位置,没有设置SHARED_QUEUE标记位
scanState是workQueue的属性,是int类型的。scanState的低16位可以用来定位当前worker处于workQueues数组的哪个位置。每个worker在被创建时会在其构造函数中调用pool的registerWorker,而registerWorker会给scanState赋一个初始值,这个值是奇数,因为worker是由createWorker创建,并会被放到WorkQueues的奇数位置,而createWorker创建worker时会调用registerWorker。 简言之,worker的scanState初始值是奇数,非worker的scanstate初始值=INACTIVE=1<<31,小于0(非worker的workQueue在externalSubmit中创建)。 当每次调用signalWork(或tryRelease)唤醒worker时,worker的高16位就会加1 另外,scanState<0表示worker未激活,当worker调用runtask执行任务时,scanState会被置为偶数,即设置scanState的最右边一位为0。
worker休眠时,是这样存储的
worker的唤醒类似这样:
在worker休眠的4行伪码中,让ctl的低32位的值变为worker.scanState,这样下次就可以通过scanState唤醒该worker。唤醒该worker时,把该worker的preStack设置为ctl低32位的值,这样下下次唤醒的worker就是scanState等于该preStack的worker。 这里通过preStack保存下一个worker,这个worker比当前worker更早地在等待,所以形成一个后进先出的栈。
runState是int类型的值,控制整个pool的运行状态和生命周期,有下面几个值(可以好几个值同时存在):
如果runState值为0,表示pool尚未初始化。 RSLOCK表示锁定pool,当添加worker和pool终止时,就要使用RSLOCK锁定整个pool。如果由于runState被锁定,导致其他操作等待runState解锁(通常用wait进行等待),当runState设置了RSIGNAL,表示runState解锁,并通知(notifyAll)等待的操作。 剩下4个值都跟runState生命周期有关,都可以顾名思义: 当需要停止时,设置runState的STOP值,表示准备关闭,这样其他操作看到这个标记位,就不会继续操作,比如tryAddWorker看到STOP就不会再创建worker:
而tryTerminate对这些生命周期状态的处理则是这样的:
当前top和base的初始值为 INITIAL_QUEUE_CAPACITY >>>1= (1 << 13)>>>1 = 8192/2。然后push一个task之后,top+=1,也就是说,top对应的位置是没有task的,最近push进来的task在top-1的位置。而base的位置则能对应到task,base对应最先放进队列的task,top-1对应最后放进队列的task。
qlock值含义:1: locked, < 0: terminate; else 0 即当qlock值位0时,可以正常操作,值=1时,表示锁定
int SQMASK=0x007e,则任何整数跟SQMASK位与后,得到的数就是偶数。 证明: 注意这里化为二进制是0111 1110,尤其注意最右边第一位是0,任何数跟最右边第一位是0的数位与后,得到的数就是偶数,因为位与之后,第一位就是0,比如s=A&SQMASK,A可以是任意整数,然后把s按二进制进行多项式展开,则有s=2 n1+2 n2 ……+2^nn,这里n≥1,所以s可以被2整除,即s是偶数。 所以一个数是奇数还是偶数,看其最右边第一位即可。
我们知道workQueue有externalPush创建的和createWorker创建的worker,两种方式创建的workQueue,其放置到workQueues的位置是不同的,前者放到workQueue的偶数位置,而后者则放到奇数位置。不同workQueue找到自己在workQueues的位置的算法有点不同。 下面看一下forkjoin框架获取workQueues中的偶数位置的workQueue的算法:
这样就能获取workQueues的偶数位置的workQueue。m保证m & r & SQMASK这整个运算结果不会超出workQueues的下标,SQMASK保证取到的是偶数位置的workQueue。这里有一个有趣的现象,假设0到workQueues.length-1之间有n个偶数,m & r & SQMASK每次都能取到其中一个偶数,而且连续n次取到的偶数不会出现重复值,散列性非常好。而且是循环的,即1到n次取n个不同偶数,n+1到2n也是取n次不同偶数,此时n个偶数每个都被重新取一次。下面分析下r值有什么秘密,为何能保证这样的散列性 ThreadLocalRandom内有一常量PROBE_INCREMENT = 0x9e3779b9,以及一个静态的probeGenerator =new AtomicInteger() ,然后每个线程的probe= probeGenerator.addAndGet(PROBE_INCREMENT)所以第一个线程的probe值是0x9e3779b9,第二个线程的值就是0x9e3779b9+0x9e3779b9,第三个线程的值就是0x9e3779b9+0x9e3779b9+0x9e3779b9以此类推,整个值是线性的,可以用y=kx表示,其中k=0x9e3779b9,x表示第几个线程。这样每个线程的probe可以保证不一样,而且具有很好的离散性。 实际上,可以不用0x9e3779b9这个值,用任意一个奇数都是可以的,比如1。如果用1的话,probe+=1,这样每个线程的probe就都是不同的,而且具有很好的离散性。也就是说,假设有限制条件probe
<n,超过n则产生溢出。则probe自加n次后才会开始出现重复值,n次前probe每次自加的值都不同。实际上用任意一个奇数,都可以保证probe自加n次后才会开始出现重复值,有兴趣可看本文最后附录部分。由于奇数的离散性,所以只要线程数小于m或者sqmask两者中的最小值,则每个线程都能唯一地占据一个ws中的一个位置
当一个操作是在非ForkjoinThread的线程中进行的,则称该操作为外部操作。比如我们前面执行pool.invoke,invoke内又执行externalPush。由于invoke是在非ForkjoinThread线程中进行的(这里是在main线程中进行),所以是一个外部操作,调用的是externalPush。之后task的执行是通过ForkJoinThread来执行的,所以task中的fork就是内部操作,调用的是push,把任务提交到工作队列。其实fork的实现是类似下面这样的:
即fork会根据执行自身的线程是否是ForkJoinThread的实例来判断是处于外部还是内部。那为何要区分内外部? 任何线程都可以使用ForkJoin框架,但是对于非ForkJoinThread的线程,它到底是怎样的,ForkJoin无法控制,也无法对其优化。因此区分出内外部,这样方便ForkJoin框架对任务的执行进行控制和优化 forkJoinPool.invoke(task)是把任务放入工作队列,并等待任务执行。源码如下
这里externalPush负责任务提交,externalPush源码如下:
</n,超过n则产生溢出。则probe自加n次后才会开始出现重复值,n次前probe每次自加的值都不同。实际上用任意一个奇数,都可以保证probe自加n次后才会开始出现重复值,有兴趣可看本文最后附录部分。由于奇数的离散性,所以只要线程数小于m或者sqmask两者中的最小值,则每个线程都能唯一地占据一个ws中的一个位置
java fork join 框架 用的多吗
用的不多,java的fork/join就是一个并行计算的框架,只能在JVM虚拟机内部进行并行计算,也就是只能在一台机器上做并行计算。一般只有在数据处理量非常大的时候才会去选择并行计算,这时候往往是用hadoop这样的分布式map/reduce框架,在多台机器上做,这样性能会更好,而且容易扩展。
java joinforkpool excute和submit的区别
ava7引入了Fork Join的概念,来更好的支持并行运算。顾名思义,Fork Join类似与流程语言的分支,合并的概念。也就是说Java7 SE原生支持了在一个主线程中开辟多个分支线程,并且根据分支线程的逻辑来等待(或者不等待)汇集,当然你也可以fork的某一个分支线程中再开辟Fork Join,这也就可以实现Fork Join的嵌套。
有两个核心类ForkJoinPool和ForkJoinTask。
ForkJoinPool实现了ExecutorService接口,起到线程池的作用。所以他的用法和Executor框架的使用时一样的,当然Fork Join本身就是Executor框架的扩展。ForkJoinPool有3个关键的方法,来启动线程,execute(...),invoke(...),submit(...)。具体描述如下:
客户端非fork/join调用 内部调用fork/join
异步执行 execute(ForkJoinTask) ForkJoinTask.fork
等待获取结果 invoke(ForkJoinTask) ForkJoinTask.invoke
执行,获取Futrue submit(ForkJoinTask) ForkJoinTask.fork(ForkJoinTasks are Futures)
多线程实现的四种方式
多线程实现的四种方式Thread裸线程、Executor服务、ForkJoin框架、Actor模型。
1、Thread裸线程
线程是并发最基本的单元。Java线程本质上被映射到操作系统线程,并且每个线程对象对应着一个计算机底层线程。每个线程有自己的栈空间,它占用了JVM进程空间的指定一部分。
线程的接口相当简明,你只需要提供一个Runnable,调用start开始计算。没有现成的API来结束线程,你需要自己来实现。
优点是很接近并发计算的操作系统/硬件模型,并且这个模型非常简单。多个线程运行,通过共享内存通讯。最大劣势是,开发者很容易过分的关注线程的数量。
线程是很昂贵的对象,创建它们需要耗费大量的内存和时间。这是一个矛盾,线程太少,你不能获得良好的并发性;线程太多,将很可能导致内存问题,调度也变得更复杂。如果你需要一个快速和简单的解决方案,你绝对可以使用这个方法,不要犹豫。
2、Executor服务
另一个选择是使用API来管理一组线程。幸运的是,JVM为我们提供了这样的功能,就是Executor接口。它隐藏了如何处理Runnable的细节。
它仅仅说,“开发者!给我任务,我会处理它!”更酷的是,Executors类提供了一组方法,能够创建拥有完善配置的线程池和executor。我们将使用newFixedThreadPool,它创建预定义数量的线程,并且不允许线程数量超过这个预定义值。
这意味着,如果所有的线程都被使用的话,提交的命令将会被放到一个队列中等待;当然这是由executor来管理的。在它的上层,有ExecutorService管理executor的生命周期,以及CompletionService会抽象掉更多细节,作为已完成任务的队列。
如果你需要精确的控制程序产生的线程数量,以及它们的精确行为,那么executor和executor服务将是正确的选择。例如,需要仔细考虑的一个重要问题是,当所有线程都在忙于做其他事情时,需要什么样的策略?
增加线程数量或者不做数量限制?把任务放入到队列等待?如果队列也满了呢?无限制的增加队列大小?
感谢JDK,已经有很多配置项回答了这些问题,并且有着直观的名字,例如上面的Executors.newFixedThreadPool(4)。
线程和服务的生命周期也可以通过选项来配置,使资源可以在恰当的时间关闭。唯一的不便之处是,对新手来说,配置选项可以更简单和直观一些。然而,在并发编程方面,你几乎找不到更简单的了。总之,对于大型系统,使用executor最合适。
3、ForkJoin框架
通过并行流,使用ForkJoinPool(FJP),Java8中加入了并行流,从此我们有了一个并行处理集合的简单方法。它和lambda一起,构成了并发计算的一个强大工具。
如果你打算运用这种方法,那么有几点需要注意。首先,你必须掌握一些函数编程的概念,它实际上更有优势。其次,你很难知道并行流实际上是否使用了超过一个线程,这要由流的具体实现来决定。如果你无法控制流的数据源,你就无法确定它做了什么。
另外,你需要记住,默认情况下是通过ForkJoinPool.commonPool实现并行的。这个通用池由JVM来管理,并且被JVM进程内的所有线程共享。这简化了配置项,因此你不用担心。
ForkJoin是一个很好的框架,当需要写一个包含并行处理的小型程序时,它是第一选择。它最大的缺点是,你必须预见到它可能产生的并发症。如果对JVM没有整体上的深入了解,这很难做到。这只能来自于经验。
4、Actor模型
JDK中没有actor的实现;因此你必须引用一些实现了actor的库。
简短地说,在actor模型中,你把一切都看做是一个actor。一个actor是一个计算实体,就像上面第一个例子中的线程,它可以从其他actor那里接收消息,因为一切都是actor。
在应答消息时,它可以给其他actor发送消息,或者创建新的actor并与之交互,或者只改变自己的内部状态。相当简单,但这是一个非常强大的概念。生命周期和消息传递由你的框架来管理,你只需要指定计算单元是什么就可以了。
另外,actor模型强调避免全局状态,这会带来很多便利。你可以应用监督策略,例如免费重试,更简单的分布式系统设计,错误容忍度等等。Akka Actors有Java接口,是最流行的JVM Actor库之一。
实际上,它也有Scala接口,并且是Scala目前默认的actor库。Scala曾经在内部实现了actor。不少JVM语言都实现了actor,比如Future。这些说明了Actor模型已经被广泛接受,并被看做是对语言非常有价值的补充。
Akka actor在内部使用ForkJoin框架来处理工作。Actor模型的强大之处来自于Props对象的接口,通过接口我们可以为actor定义特定的选择模式,定制的邮箱地址等。结果系统也是可配置的,只包含了很少的活动件。
这是一个很好的迹象!使用Actor模型的一个劣势是,它要求你避免全局状态,因此你必须小心的设计你的应用程序,而这可能会使项目迁移变得很复杂。同时,它也有不少优点,因此学习一些新的范例和使用新的库是完全值得的。
可以看出Scala非常简单,它的并发线程你无需跟线程啊,锁啊,线程间通信,线程间协同等难题打交道。它把这些都封装起来了。
fpga中always语句什么时候执行
begin end 之间的代码是串行执行的,begin end间的<=也是串行执行的,比如:
begin
a <= b;
a <= c;
d <= a;
end
最后a<=c起作用,a<=b相当于没起作用,是串行。
a最后的值是c,而不是x(如果是并行,因为多驱动,值会是x)。
只不过赋给d的是a的老值,而不是c,这(非阻塞赋值)是用串行模拟并行的一种方法。
但这并不是说begin end间的<=是并行,begin end间的代码是串行的。
fork join间的是并行。
多个always和initial间是并行。
FPGA中,if后可以接并行语句块,fork……join吗
用begin-end,fork-join不能被综合的!
如果你用ip核生成ram在控制的话,可以再定义ip核的时候加上读写使能信号,在一个状态读ram1的时候:ram1 读使能有效,写使能无效;ram2与它相反;在另一个状态时与前一个状态相反。
调用ip核定制ram来控制还是比较简单的,两个交互控制就像乒乓操作!
SystemVerilog中的Process(2)--- 进程的控制
Hello everybody,我们接着上期的Process(1)-产生进程的方式( 点击跳转 )继续讲解SystemVerilog中对于process的多种控制方式。
本期黄鸭哥主要给大家讲解 named block、wait_order、wait_fork、disable,还有SystemVerilog中的内建类:process类。
1、 Named?block
Block,也就是语句块,SystemVerilog提供了两种类型的语句块,分别是begin…end为代表的顺序语句块,还有以fork…join为代表的并发语句块。
这两种block都是工程项目中常用的block,但是,大家可能都不知道block也是可以命名的,就像我们每个人的名字一样,名字是我们每个人的一个标识。
通过这个标识,我们可以访问block中的变量、parameter等。
图1中,initial begin…end中有两个named block,分别为顺序执行的block_a和并发执行的block_b。
我们可以通过在block的开头和结束编写上标识名,也可以只在开头进行编写;
如果是在block的开头和结束都有对应的标识名,则这两标识名必须相同,否则会编译报错。
从图1和图2中我们还可以看到,int类型的变量和parameter的作用范围都是在各自block范围内的。
所以,我们要记住,如果需要访问block中的变量或者parameter,则需要给block进行命名,并且,block中的变量、parameter都是相互独立的。
2、 wait_order、wait fork
SystemVerilog提供了两大类process的控制方式,分别为wait和disable。
wait中又包含了三小类:wait、wait_order、wait fork。
其中,wait相对简单,在这就不说了,黄鸭哥只介绍后两种。
那么,什么是wait_order?
黄鸭哥总结一句:wait_order会阻塞等待多个事件的触发,并且要求这几个事件间发生的顺序和设置的一致。
这句话该怎么理解呢?我们来看下例子:
例子中包含3个event,3个event分别在3个并发进程中进行触发,event_1在第50个timeunit被触发,event_2在第30个timeunit被触发,event_3在第100个timeunit的时候被触发。
wait_order需要阻塞到3个事件依次按照event_2、event_1、event_3的顺序进行触发后才能往下执行。这就是wait_order的用法。
wait中另外一个重要的用法是wait fork,关于它的用法,黄鸭哥总结了三句话:
① wait fork会引起调用进程阻塞,直到它的所有子进程结束;
② wait fork的目的是用来确保所有子进程执行结束;
③ wait fork作用父进程下的子进程,而不包括子进程下的子进程。
图4中initial语句块包含4个子进程,proc_1~proc_4。
其中,proc_4子进程中还含有一个子进程proc_4_1,此进程就是initial语句块的子进程的子进程。
按照上面黄鸭哥总结的三点来看,wait fork应该只需要等待到第2个子进程:proc_2执行结束,因为wait fork只会作用到子进程,不会作用到子进程的子进程(不论是fork…join,fork…join_any还是fork…join_none产生的进程)。
我们来看下仿真结果验证下我们的理论:
果然,wait fork只等待到proc_2,在第200个timeunit就打印出了“wait fork finish”。
3、 disable 、disable fork
上面说完了wait,我们接着说SystemVerilog中第二大类的进程控制方式:disable。
disable语句常用的多种方式包括:disable named_block、disable task_name和disable fork等,调用disable语句将会终止指定的进程。
图6里调用了disable,终止了对应的带标识名的block,从仿真截图中可以看出,block_1并没有正常结束,而是中途被终止了。
上图中disable proc_a相当于return,它会直接终止当前的task。
而关于disable fork,这个是我们工程项目中使用的最多的一类,也是最容易踩坑的一类,disable fork的用法我推荐大家学习下Q哥的一篇文章: disable fork,你真的会用吗?
感兴趣的同学可以点进去看看,他在里面有很详细的描述,黄鸭哥在这里就不多说了。
4、 内建类:process
SystemVerilog中内建了一种class,可以对进程进行访问和控制,此种class就是process,我们先来看下process类的原型:
process类中定义了一个枚举变量state,表示当前进程的几种执行状态:FINISHED,RUNNING,WAITING,SUSPENDED,KILLED。
另外,还声明了几种task和function,我们通过下面的表格来简单了解下:
有人会问了,这么多的method,我们都该记住吗?
黄鸭哥觉得不是的,只需要记住这里面常用的几个method:self(),status(),kill()就可以了,我们最后再来看个例子,加深下理解:
图9展示了这三个method的用法,通过self()获取进程的句柄,然后传递给定义的两个process类;
调用status()可以获取到process_1和process_2进程的执行状态,在100个timeunit的时候,process_1已经执行结束,而process_2则处于阻塞状态;
当再次经过100个timeunit时,调用kill()终止了process_2的继续执行,从这里可以看出,要终止一个进程,除了disable、disable fork之外,还可以调用process.kill()。
总结
process就讲到这了,我们再回顾下今天的要点:
1、block可以进行命名,命名之后就可以通过标识名访问block内部的变量和parameter,还可以通过disable named_block终止此block。
2、进程的两大控制方式:wait、disable。
3、SystemVerilog内建类,process类,可以通过process类访问进程和控制进程。
4、三种终止进程的方式:disable、disable fork、process.kill()。
快乐的时光总是很短暂,又到了说拜拜的时候了,下次黄鸭哥将给你们带来新的主题。
别着急问新主题是什么,留着点悬念,下次将带给你们新的惊喜!
Stream并行流详解
在说到并行的时候,相信很多人都会想到并发的概念。那么并行和并发两者一字之差,有什么区别呢?
并行:多个任务在同一时间点发生,并由不同的cpu进行处理,不互相抢占资源
并行:
并发:多个任务在同一时间点内同时发生了,但由同一个cpu进行处理,互相抢占资源
并发:
当在大量数据处理上,数据并行化可以大量缩短任务的执行时间,将一个数据分解成多个部分,然后并行处理,最 后将多个结果汇总,得到最终结果。
对于并行流,其在底层实现中,是沿用了Java7提供的fork/join分解合并框架进行实现。fork根据cpu核数进行数 据分块,join对各个fork进行合并。实现过程如下所示:
对于并行流,一定不要陷入一个误区:并行一定比串行快。并行在不同的情况下它不一定是比串行快的。影响并行 流性能主要存在5个因素:
1)数据大小:输入数据的大小,直接影响了并行处理的性能。因为在并行内部实现中涉及到了fork/join操作,它 本身就存在性能上的开销。因此只有当数据量很大,使用并行处理才有意义。
2)源数据结构:fork时会对源数据进行分割,数据源的特性直接影响了fork的性能。 ArrayList、数组或IntStream.range,可分解性最佳,因为他们都支持随机读取,因此可以被任意分割。 HashSet、TreeSet,可分解性一般,其虽然可被分解,但因为其内部数据结构,很难被平均分解。 LinkedList、Streams.iterate、BufferedReader.lines,可分解性极差,因为他们长度未知,无法确定在哪里进行 分割。
3)装箱拆箱 尽量使用基本数据类型,避免装箱拆箱。
4)CPU核数 fork的产生数量是与可用CPU核数相关,可用的核数越多,获取的性能提升就会越大。
5)单元处理开销 花在流中每个元素的时间越长,并行操作带来的性能提升就会越明显。
1)基本类型
性能消耗: Stream串行>for循环>Stream并行
2)对象
性能消耗: Stream串行>for循环>Stream并行
3)复杂对象
性能消耗: for循环>Stream串行>Stream并行
结论: 对于简单操作,如果环境机是多核的话,建议使用Stream并行,同时在不考虑核数的情况 下,普通for循环性能要明显高于Stream串行,相差两倍左右。 对于复杂操作,推荐使用Stream API操作。