JUC - 多线程之ForkJoin
ForkJoin
ForkJoin是在Java7提供的一个用于并行执行任务的框架,ForkJoin从字面意思上看Fork是分叉的意思,Join是结合的意思,核心思想就是把大任务分割成若干个小任务,最终汇总每个小任务结果后得到大任务结果,其实现思想与MapReduce差不多。
ForkJoin体系中最为关键的就是ForkJoinTask和ForkJoinPool,ForkJoin就是利用分治的思想将大的任务按照一定规则Fork拆分成小任务,再通过Join聚合起来;
ForkJoin最经典的一个应用就是Java8中的Stream,我们知道Stream分为串行流和并行流,其中并行流parallelStream就是依赖于ForkJoin来实现并行处理的;
Forkjoin主要使用两个类
1、ForkJoinTask
ForkJoinTask : 基本任务,使用fork、join框架必须创建的对象,提供fork,join操作,常用的三个子类如下:
RecursiveAction:无结果返回的任务RecursiveTask:有返回结果的任务CountedCompleter:无返回值任务,完成任务后可以触发回调
ForkJoinTask提供了两个重要的方法:
(1)fork:让task异步执行,类似于线程的Thread.start()方法,但是它不是真的启动一个线程,而是将任务放入到工作队列中。
(2)join:让task同步执行,可以获取返回值,类似于线程的Thread.join()方法,但是他不是简单的阻塞线程,而是利用工作线程运行其他任务,当一个工作线程调用了join()方法,它将处理其他任务,直到注意到目标子任务已经完成了。
2、ForkJoinPool
ForkJoinPool:专门用来运行ForkJoinTesk的线程池,在实际使用,也可以接受Runnable/Callable任务,但是在真正运行时,也会把这些任务封装成ForkJoinTesk类型的任务;
这是ForkJoin框架的核心,是ExecutorService的一个实现,用于管理工作线程,并提供一些工具来帮助获取有关线程池状态和性能的信息,工作线程异常只能执行一个任务;
ForkJoinPool并不会为每一个子任务创建一个单独的线程,相反,线程池中的每个线程都有自己的双端队列用于存储任务(double-ended queue).
这种架构使用了一种名为工作窃取(work-stealing)算法来平衡线程的工作负载。
3、ForkJoinPool内部原理
ForkJoinPool内部使用的是“工作窃取”算法实现的。
- 每个工作线程都有自己的工作队列WorkQueue
- 这是一个双端队列,它是线程私有的
- ForkJoinTesk中的fork子任务,将放入运行任务的工作线程的队头,工作线程将以LIFO的顺序来处理工作队列中的任务
- 为了最大化地利用CPU,空闲的线程将从其他线程的队列中“窃取”任务来执行
- 从工作队列的尾部窃取任务,以减少竞争
- 双端队列的操作:
push()/pop()仅在其所有者工作线程中调用,poll()是由其他线程窃取任务时调用的; - 当只剩下最后一个任务时,还是会存在竞争是通过CAS来实现的;
总结
- 最适合的是计算密集型任务;
- 在需要阻塞工作线程时,可以使用
ManagedBlocker; - 不应该在
RecursiveTask的内部使用ForkJoinPool.invoke()/invokeAll(); - ForkJoinPool特别适合于“分而治之”算法的实现;
- ForkJoinPool和ThreadPoolExecutor是互补的,不是谁替代谁的关系,二者适用的场景不同;
- ForkJoinTask有两个核心方法——fork()和join(),有三个重要子类——
RecursiveAction、RecursiveTask和CountedCompleter; - ForkjoinPool内部基于“工作窃取”算法实现;
- 每个线程有自己的工作队列,它是一个双端队列,自己从队列头存取任务,其它线程从尾部窃取任务;
- RecursiveTask内部可以少调用一次fork(),利用当前线程处理,这是一种技巧;
什么是ManageBlocker
ManagedBlocker相当于明确告诉ForkJoinPool框架要阻塞了,ForkJoinPool就会启另一个线程来运行任务,以最大化地利用CPU。