Java 1.7
引入了一种新的并发框架—— Fork/Join Framework
Fork/Join
的思路:通过分而治之(把大任务分割成若干个小任务),只不过划分之后的任务更适合分派给不同的计算资源,可以并行的完成任务。
原理
Fork/Join
框架主要依靠fork
和join
两个操作,一般对这两个操作的解释如下:
fork()
:开启一个新线程(或是重用线程池内的空闲线程),将任务交给该线程处理。join()
:等待该任务的处理线程处理完毕,获得返回值。
这里有个问题,不断的fork()
如果是不断创建线程,岂不是要“线程数量爆炸”?事实上,ForkJoinPool
用了一种work stealing
的算法,避免产生大量线程。所以如果一开始设置线程池的线程数为N,实际上使用ForkJoinPool
的时候也只会有固定的线程数(默认和CPU核数一样)。
Fork/Join的基本用法
|
|
ForkJoin框架组成
ForkJoinPool
:管理worker
线程,类似ThreadPoolExecutor
,提供接口用于提交或者执行任务;ForkJoinWorkerThread
:worker
线程,任务保存在一个deque
中;ForkJoinTask
:ForkJoin
框架中运行的任务,可以fork
子任务,可以join
子任务完成。
ForkJoinPool构造函数
|
|
work stealing(窃取)算法
一个任务通过fork()
被分割成若干个小任务。比如线程1和线程2都被分割成4个小任务,如果线程1执行完毕,那么他可以去窃取线程2的工作。当要发生线程窃取的时候,两个线程内的任务可以理解成放在一个线程自己的双端队列中。例如下图线程2中的任务被分割成若干个小任务(就WorkQueue
里面的一个个小方块)放在其线程的双端队列中。被窃取任务线程从其他双端队列的头部拿任务执行,而窃取任务的线程永远从双端队列的尾部拿任务执行。
ForkJoinPool
的每个工作线程都维护着一个工作队列(WorkQueue
),这是一个双端队列(Deque),里面存放的对象是任务(ForkJoinTask
)。- 每个工作线程在运行中产生新的任务(通常是因为调用了
fork()
)时,会放入工作队列的队尾,并且工作线程在处理自己的工作队列时,使用的是 LIFO 方式,也就是说每次从队尾取出任务来执行。 - 每个工作线程在处理自己的工作队列同时,会尝试窃取一个任务(或是来自于刚刚提交到 pool 的任务,或是来自于其他工作线程的工作队列),窃取的任务位于其他线程的工作队列的队首,也就是说工作线程在窃取其他工作线程的任务时,使用的是 FIFO 方式。
- 在遇到
join()
时,如果需要join
的任务尚未完成,则会先处理其他任务,并等待其完成。 - 在既没有自己的任务,也没有可以窃取的任务时,进入休眠
优缺点
work stealing
算法的优点:利用了线程进行并行计算,减少了线程间的竞争。
work stealing
算法的缺点:
- 如果双端队列中只有一个任务时,线程间会存在竞争。
- 额外的开销,例如双端队列
fork()
fork()
做的工作只有一件事,既是把任务推入当前工作线程的工作队列里。
|
|
join()
- 检查调用
join()
的线程是否是ForkJoinThread
线程。如果不是,则阻塞当前线程,等待任务完成。如果是,则不阻塞。 - 查看任务的完成状态,如果已经完成,直接返回结果。
- 如果任务尚未完成,但处于自己的工作队列内,则完成它。
- 如果任务已经被其他的工作线程偷走,则窃取这个任务的worker执行(以 FIFO 方式),以期帮助它早日完成
join
的任务。 - 如果偷走任务的worker也已经把自己的任务全部做完,正在等待需要
join
的任务时,则找到该小偷的小偷,帮助它完成它的任务。 - 递归地执行第5步。
|
|
案例
统计1~1000整数之和
单线程For循环
|
|
ExecutorService线程池
把大任务分割成若干个小任务,并行计算再合并结果
|
|
|
|
ForkJoinPool线程池
ForkJoinPool
主要用于实现“分而治之”的算法,特别是分治之后递归调用的函数,例如quick sort
等
|
|
在这段代码里没有显式地“把任务分配给线程”,只是分解了任务,而把具体的任务到线程的映射交给了 ForkJoinPool
来完成。