面试大厂必问的ForkJoin框架剖析【建议收藏】

点关注,不迷路!如果本文对你有帮助的话不要忘记点赞支持哦!

概述

image.png

和传统的线程池使用AQS的实现逻辑不同,ForkJoin引入全新的结构来标识:

  • ForkJoinPool: 用于执行ForkJoinTask任务的执行池,不再是传统执行池 Worker+Queue 的组合模式,而是维护了一个队列数组WorkQueue,这样在提交任务和线程任务的时候大幅度的减少碰撞。
  • WorkQueue: 双向列表,用于任务的有序执行,如果WorkQueue用于自己的执行线程Thread,线程默认将会从top端选取任务用来执行 - LIFO。因为只有owner的Thread才能从top端取任务,所以在设置变量时, int top; 不需要使用 volatile
  • ForkJoinWorkThread: 用于执行任务的线程,用于区别使用非ForkJoinWorkThread线程提交的task;启动一个该Thread,会自动注册一个WorkQueue到Pool,这里规定,拥有Thread的WorkQueue只能出现在WorkQueue数组的奇数位
  • ForkJoinTask: 任务, 它比传统的任务更加轻量,不再对是RUNNABLE的子类,提供fork/join方法用于分割任务以及聚合结果。
  • 为了充分施展并行运算,该框架实现了复杂的 worker steal算法,当任务处于等待中,thread通过一定策略,不让自己挂起,充分利用资源,当然,它比其他语言的协程要重一些。

ForkJoinPool变量基本说明

作为框架的提交入口,ForkJoinPool管理着线程池中线程和任务队列,标识线程池是否还接收任务,显示现在的线程运行状态。本节,对这些控制量进行解释。

如果读者看过 类似 disrupter 这种高效率队列的开源实现,大家肯定会对cache line记忆犹新,他们通常的做法自己设置伪变量来填充,jdk1.8�中官网为我们带来了sun.misc.Contended,所以你如果阅读ForkJoinPool源码可以发现该类也被sun.misc.Contended标识。
几个重要变量:

  • runState: 标识Pool运行状态,使用bit位来标识不同状态,比如
        // runState bits: SHUTDOWN must be negative, others arbitrary powers of two
      private static final int  RSLOCK     = 1;
      private static final int  RSIGNAL    = 1 << 1;
      private static final int  STARTED    = 1 << 2;
      private static final int  STOP       = 1 << 29;
      private static final int  TERMINATED = 1 << 30;
      private static final int  SHUTDOWN   = 1 << 31;
    
    如果执行 runState & RSLOCK ==0 就能直接说明,目前的运行状态没有被锁住,其他情况一样。
  • config:parallelism | mode
  • parallelism: 这个变量不是内部定义的变量,但是需要各位注意一下它的界限,因为后面的处理需要注意
    static final int MAX_CAP      = 0x7fff;        // max #workers - 1
    

也就是说他最大就占16位

  • ctl:ctl是Pool的控制变量,类型是long - 说明有64位,每个部分都有不同的作用。我们使用十六进制来标识ctl,依次说明不同部分的作用。
    long np = (long)(-parallelism); // offset ctl counts
    this.ctl = ((np << AC_SHIFT) & AC_MASK) | ((np << TC_SHIFT) & TC_MASK);
    0x xxxx-1  xxxx-2  xxxx-3  xxxx-4
    
    我为每个部分使用了数字来标识 - 1,2,3,4。
    • 编号为1的16位: AC 表示现在获取的线程数,这里的初始化比较有技巧,使用的是并行数的相反数,这样如果active的线程数,还没到达了我们设置的阈值的时候,ctl是个负数,我们可以根据ctl的正负直观的知道现在的并行数达到阈值了么。
    • 编号为2的16位:TC 表示线程总量,初始值也是并行数的相反数。这里需要说明一下,这个编号1所表示的活跃的线程数的区别,我们虽然开启了并行数等量的线程,但是可能在某些条件下,运行的thread不得不wait或者park,原因我们后面会提到,这个时候,虽然我们开启的线程数量是和并行数相同,但是实际真正执行的却不是这么多。TC 记录了我们一共开启了多少线程,而AC则记录了没有挂起的线程。
    • 编号为3的16位:后32位标识 idle workers 前面16位第一位标识是active的还是inactive的,其他为是版本标识。
    • 编号为4的16位:标识idle workersWorkQueue[]数组中的index。这里需要说明的是,ctl的后32位其实只能表示一个idle workers,那么我们如果有很多个idle worker要怎么办呢?老爷子使用的是stack的概念来保存这些信息。后32位标识的是top的那个,我们能从top中的变量stackPred追踪到下一个idle worker

WorkQueue变量基本说明

WorkQueue是一个双向列表,用于task的排队。
几个变量的定义说明:

  • scanState: // versioned, <0: inactive; odd:scanning 如果WorkQueue没有属于自己的owner(下标为偶数的都没有),该值为 inactive 也就是一个负数。如果有自己的owner,该值的初始值为其在WorkQueue[]数组中的下标,也肯定是个奇数。
    如果这个值,变成了偶数,说明该队列所属的Thread正在执行Task

        static final int SCANNING     = 1;             // false when running   tasks
        static final int INACTIVE     = 1 << 31;       // must be negative
    
  • stackPred: 记录前任的 idle worker

  • config:index | mode。 如果下标为偶数的WorkQueue,则其mode是共享类型。如果有自己的owner 默认是 LIFO

    什么时候应该设置成 FIFO,注释中这么给的建议:
    establishes local first-in-first-out scheduling mode for forked
    tasks that are never joined. This mode may be more appropriate
    than default locally stack-based mode in applications in which
    worker threads only process event-style asynchronous tasks.
    For default value, use {@code false}

  • qlock: 锁标识,在多线程往队列中添加数据,会有竞争,使用此标识抢占锁。

  • base:worker steal的偏移量,因为其他的线程都可以偷该队列的任务,所有base使用volatile标识。

  • top:owner执行任务的偏移量。

  • parker:如果 owner 挂起,则使用该变量做记录。

  • currentJoin: 当前正在join等待结果的任务。

  • currentSteal:当前执行的任务是steal过来的任务,该变量做记录。

ForkJoinTask变量基本说明

  • status: 标识任务目前的状态,如果<0,表示任务处于结束状态。
    ((s >>> 16) != 0)表示需要signal其他线程

任务提交过程剖析

ForkJoinPool提供的提交接口很多,不管提交的是CallableRunnableForkJoinTask最终都会转换成ForkJoinTask类型的任务,调用方法externalPush(ForkJoinTask<?> task)来进行提交逻辑。让我们来看看提交的过程:

  • 如果第一次提交(或者是hash之后的队列还未初始化),调用externalSubmit

    • 第一遍循环: (runState不是开始状态): 1.lock; 2.创建数组WorkQueue[n],这里的n是power of 2; 3. runState设置为开始状态。
    • 第二遍循环:(根据ThreadLocalRandom.getProbe()hash后的数组中相应位置的WorkQueue未初始化): 初始化WorkQueue,通过这种方式创立的WorkQueue均是SHARED_QUEUE,scanStateINACTIVE
    • 第三遍循环: 找到刚刚创建的WorkQueue,lock住队列,将数据塞到arraytop位置。如果添加成功,就用调用接下来要摊开讲的重要的方法signalWork
  • 如果hash之后

  • 6
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 5
    评论
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值