Java并发-ForkJoin


  • 线程数计算方法:线程数 = CPU 核心数 *(1+平均等待时间/平均工作时间)
    • CPU密集型任务:线程数为 CPU 核心数的 1~2 倍
    • IO密集型任务:线程数一般会大于 CPU 核心数很多倍
  • 分治算法:将一个规模为N的问题分解为K个规模较小的子问题,这些子问题相互独立且与原问题性质相同。求出子问题的解,就可得到原问题的解
    • 步骤:分解;求解;合并
    • 在分治法中,子问题一般是相互独立的,因此,经常通过递归调用算法来求解子问题
  • Fork/Join:ForkJoinPool允许其他线程向它提交任务,并根据设定将这些任务拆分为粒度更细的子任务,这些子任务将由ForkJoinPool内部的工作线程来并行执行,并且工作线程之间可以窃取彼此之间的任务
    • 最适合计算密集型任务,而且最好是非阻塞任务
  • ForkJoinPool 是用于执行 ForkJoinTask 任务的执行池
    • 维护了一个队列数组 WorkQueue(WorkQueue[]),这样在提交任务和线程任务的时候大幅度减少碰撞
    • 四个核心参数:并行数、工作线程的创建、异常处理和模式指定
    • 任务提交
      • 提交异步执行:execute
      • 等待并获取结果:invoke
      • 提交执行获取Future结果:submit
  • ForkJoinTask 定义了任务执行时的具体逻辑和拆分逻辑,继承了Future接口
    • fork() 提交任务:用于向当前任务所运行的线程池中提交任务。如果当前线程是ForkJoinWorkerThread类型,将会放入该线程的工作队列,否则放入common线程池的工作队列中
    • join() 获取任务执行结果:调用join()时,将阻塞当前线程直到对应的子任务完成运行并返回结果
    • 通常情况下我们不需要直接继承ForkJoinTask类,而只需要继承它的子类
      • RecursiveAction 用于递归执行但不需要返回结果的任务
      • RecursiveTask 用于递归执行需要返回结果的任务
      • CountedCompleter<T> 在任务完成执行后会触发执行一个自定义的钩子函数
  • ForkJoinPool 的工作原理
    • ForkJoinPool 内部有多个工作队列,当我们通过 ForkJoinPool 的 invoke() 或者 submit() 方法提交任务时,ForkJoinPool 根据一定的路由规则把任务提交到一个工作队列中,如果任务在执行过程中会创建出子任务,那么子任务会提交到工作线程对应的工作队列中
    • ForkJoinPool 的每个工作线程都维护着一个工作队列(WorkQueue),这是一个双端队列(Deque),里面存放的对象是任务(ForkJoinTask)
    • 每个工作线程在运行中产生新的任务(通常是因为调用了 fork())时,会放入工作队列的top,并且工作线程在处理自己的工作队列时,使用的是 LIFO 方式,也就是说每次从top取出任务来执行
    • 每个工作线程在处理自己的工作队列同时,会尝试窃取一个任务,窃取的任务位于其他线程的工作队列的base,也就是说工作线程在窃取其他工作线程的任务时,使用的是FIFO 方式
    • 在遇到 join() 时,如果需要 join 的任务尚未完成,则会先处理其他任务,并等待其完成
    • 在既没有自己的任务,也没有可以窃取的任务时,进入休眠
  • 工作窃取:就是允许空闲线程从繁忙线程的双端队列中窃取任务
    • 减少线程竞争任务的可能性:工作线程从它自己的双端队列的头部获取任务。但是,当自己的任务为空时,线程会从其他繁忙线程双端队列的尾部中获取任务
    • 工作窃取队列(work-stealing queues ):由内部类WorkQueue实现,它是Deques的特殊形式,但仅支持三种操作方式:push、pop和poll(也称为窃取)
    • 队列的读取有着严格的约束,push和pop仅能从其所属线程调用,而poll则可以从其他线程调用
  • 为什么工作线程总是从头部获取任务,窃取线程从尾部获取任务
    • 通过始终选择最近提交的任务,可以增加资源仍分配在CPU缓存中的机会
    • 窃取者之所以从尾部获取任务,则是为了降低线程之间的竞争可能
    • 由于任务是可分割的,那队列中较旧的任务最有可能粒度较大,因为它们可能还没有被分割,而空闲的线程则相对更有“精力”来完成这些粒度较大的任务
  • 工作队列 WorkQueue
    • WorkQueue 是双向列表,用于任务的有序执行,如果 WorkQueue 用于自己的执行线程 Thread,线程默认将会从尾端选取任务用来执行 LIFO
    • 每个 ForkJoinWorkThread 都有属于自己的 WorkQueue,但不是每个 WorkQueue 都有对应的 ForkJoinWorkThread
    • 没有 ForkJoinWorkThread 的 WorkQueue 保存的是 submission,来自外部提交,在 WorkQueues[] 的下标是偶数位
  • ForkJoinWorkThread 是用于执行任务的线程,用于区别使用非 ForkJoinWorkThread 线程提交task。启动一个该 Thread,会自动注册一个 WorkQueue 到 Pool,拥有 Thread 的 WorkQueue 只能出现在 WorkQueues[] 的奇数位


文章作者: 钱不寒
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 钱不寒 !
  目录