JUC - 多线程之ForkJoin

133 阅读3分钟

JUC - 多线程之ForkJoin

ForkJoin

ForkJoin是在Java7提供的一个用于并行执行任务的框架,ForkJoin从字面意思上看Fork是分叉的意思,Join是结合的意思,核心思想就是把大任务分割成若干个小任务,最终汇总每个小任务结果后得到大任务结果,其实现思想与MapReduce差不多。

ForkJoin体系中最为关键的就是ForkJoinTask和ForkJoinPool,ForkJoin就是利用分治的思想将大的任务按照一定规则Fork拆分成小任务,再通过Join聚合起来;

ForkJoin最经典的一个应用就是Java8中的Stream,我们知道Stream分为串行流和并行流,其中并行流parallelStream就是依赖于ForkJoin来实现并行处理的

ForkJoin11.png

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内部使用的是“工作窃取”算法实现的。

ForkJoin22.png

  • 每个工作线程都有自己的工作队列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。