Java Fork/Join框架学习(二)


4. 磨刀不误砍柴工,基础知识学习

4.1 ForkJoinPool的初始化

  • ForkJoinPool对外提供了三个public构造函数,这些构造函数最终都将调用private构造函数
  • 除了可以通过构造函数初始化ForkJoinPool外,ForkJoinPool还提供了一个已经构造好的common池。
  • 外部类可以通过public static类型的commonPool()方法,访问ForkJoinPool的common池

4.1.1 三个public构造函数

  • 无参构造函数:通常情况下,并发度的默认值为Runtime.availableProcessors()

    public ForkJoinPool() {
    	// MAX_CAP = 0x7fff,Runtime.availableProcessors()由系统决定,一般不会超过MAX_CAP
        this(Math.min(MAX_CAP, Runtime.getRuntime().availableProcessors()),
             defaultForkJoinWorkerThreadFactory, null, false);
    }
    
  • 指定线程池并发度的构造函数

    public ForkJoinPool(int parallelism) {
        this(parallelism, defaultForkJoinWorkerThreadFactory, null, false);
    }
    
  • 以上两个public构造函数,都将调用如下构造函数

    public ForkJoinPool(int parallelism,
                        ForkJoinWorkerThreadFactory factory,
                        UncaughtExceptionHandler handler,
                        boolean asyncMode) {
        this(checkParallelism(parallelism), // checkParallelism()保证并发度最少为1,最大为MAX_CAP
        	 // checkFactory()保证factory不为null,默认值为defaultForkJoinWorkerThreadFactory,
             checkFactory(factory), 
             handler, // worker中用于处理异常的handler,默认值为null
             // 简单地说:用于规定worker访问关联的队列时,使用FIFO序 or LIFO序?
             asyncMode ? FIFO_QUEUE : LIFO_QUEUE, 
             // 线程池中,worker的名字前缀;poolId是从1开始,逐渐自增的int值
             "ForkJoinPool-" + nextPoolId() + "-worker-"); 
        checkPermission();
    }
    
  • 关于asyncMode

    • 值为true,表示worker按照FIFO序执行fork出的子任务,且这些子任务不能被合并(join)
    • FIFO序,适用于worker只处理事件类型的异步任务场景(也是为啥该字段叫asyncMode,异步模式的原因吧)
    • 其默认值为默认值为false,表示worker将按照LIFO序执行队列中的任务

4.1.2 private构造函数

  • public构造函数,最终将调用private构造函数。

    private ForkJoinPool(int parallelism,
                         ForkJoinWorkerThreadFactory factory,
                         UncaughtExceptionHandler handler,
                         int mode,
                         String workerNamePrefix) {
        this.workerNamePrefix = workerNamePrefix;
        this.factory = factory;
        this.ueh = handler;
        this.config = (parallelism & SMASK) | mode;
        long np = (long)(-parallelism); // offset ctl counts
        this.ctl = ((np << AC_SHIFT) & AC_MASK) | ((np << TC_SHIFT) & TC_MASK);
    }
    
4.1.2.1 ForkJoinPool.config变量
  • config:高16位用于表示队列的模式,低16位(准确地说是低15位)用于表示并发度(parallelism)

关于MAX_CAP

  • parallelism经过checkParallelism()方法修正后,其最大值为MAX_CAP = 0x7fff

    private static int checkParallelism(int parallelism) {
        if (parallelism <= 0 || parallelism > MAX_CAP)
            throw new IllegalArgumentException();
        return parallelism;
    }
    
  • SMASK = 0xffff,所以parallelism & SMASK的值实际就是parallelism,一个低15位有效的int值

  • 源码对MAX_CAP的注释为max #workers - 1,即MAX_CAP = max_worker_number - 1,也就是说,worker的最大数量2^15

  • 疑问: MAX_CAP明明用于限定parallelism的最大值,叫MAX_PARALLELISIM是不是更好?

    • 从externalSubmit()方法对workQueues数组的初始化,不难发现:parallelism间接决定了workQueues数组的容量
    • parallelism的最大值,也就间接限制了workQueues数组的最大容量
    • 这也是Doug Leaw,ForkJoinPool的作者,JDK源码大神,为何将其命名为MAX_CAP的原因

队列的mode

  • 创建ForkJoinPool时,mode的值只有两类:LIFO_QUEUEFIFO_QUEUE,用于规定worker如何处理当前队列中fork出的子任务(这里的队列,是指关联了worker的worker queue)
    static final int LIFO_QUEUE   = 0;
    static final int FIFO_QUEUE   = 1 << 16;
    
  • mode还有一个值SHARED_QUEUE,用于表示队列为shared queue,该值为负数-2147483648
    static final int SHARED_QUEUE = 1 << 31;       // must be negative
    
  • 不管mode的具体值是多少,其有效值始终位于高16位。同时,第17位表示线程池是否使用FIFO的任务调度模式,即asyncMode为true

如何从config中获取parallelism和mode?

  • 通过(parallelism & SMASK) | mode计算得到的config,可以认为其高16位表示mode,低16位表示parallelism

  • 因此,想要获取队列的模式,只需要将config & MODE_MASK即可

    static final int MODE_MASK    = 0xffff << 16;  // top half of int
    int mode = config & MODE_MASK;
    
  • 而想要获取线程池的parallelism,只需要将config & SMASK即可

    static final int SMASK        = 0xffff;        // short bits == max index
    // config没有加锁,可能会读取到脏数据,需要进行修正
    public int getParallelism() {
        int par;
        return ((par = config & SMASK) > 0) ? par : 1;
    }
    
  • 与config有关的典型位运算:

    // FIFO_QUEUE = 1 << 16, 如果采用FIFO序,则config & FIFO_QUEUE = 1
    public boolean getAsyncMode() {
        return (config & FIFO_QUEUE) != 0; 
    }
    
4.1.2.2 ForkJoinPool.ctl变量
  • ctl:在源码中,对ctl的注释为main pool control。在本人看来,ctl在ForkJoinPool中掌管着worker的生死,包括:添加(add)、停用(inactivate)、重用(re-active)等

  • ctl是一个long类型的变量,每16位为一个子域,各子域的名称以及描述如下

    位置子域描述
    64 ~ 49 bitAC1. 记录活跃的#workers(表示worker数),AC = running #workers - parallelism。
    2. 如果AC < 0,说明活跃的worker不够,需要唤醒空闲的worker或添加新的worker
    48 ~ 33 bitTC1. 记录worker的总数,TC = total #workers - parallelism。
    2. 如果TC < 0,说明还可以创建新的worker
    32 ~ 17 bitSS1. 栈顶处于等待状态的worker的版本数和状态。
    2. 所谓于等待状态,是指worker处于空闲状态,等待有任务可以处理。因此,处于等待状态的worker,又叫idle worker
    3. 每一个worker在挂起时,都会存储前一个worker的队列索引。这些worker相互关联,形成了一个Treiber stack,叫做idle worker stackpool stack
    4. Treiber stack的栈顶,是最新的空闲worker。
    16 ~ 1 bitID1. 记录栈顶空闲worker的队列索引
    2. sp=(int)ctl,取ctl的低32位,如果sp != 0,说明存在空闲worker;反之,则不存在空闲worker。
    3. 后续学习了WorkQueue后,会发现sp对应的是WorkQueue的scanState字段。
    4. 在ctl中存储栈顶空闲worker的scanState,可以管理和定位一个worker是否处于INACTIVESCANNING(扫描任务)状态
  • 笔者对Treiber stack的理解

    • 通过保存前一个元素的引用,形成一个引用链;同时,只对外暴露引用链最上端的元素
    • 在这里定义上端、下端:A 保存B的引用,则A相对B,A是引用链更上端的元素;反之,B是引用链更下端的元素
    • 想要访问引用链下端的元素,必须通过最上端的元素向下溯源
    • 想要添加新的元素,也必须在最上端元素之上添加
    • 这样的访问形式,就像数据结构stack一样
  • 计算ctl前,需要将parallelism做一次转化,将其转为64位的相反数(负数)

    long np = (long)(-parallelism); // offset ctl counts
    
  • parallelism的值为15为例,np在计算机中将使用补码表示,其二进制值为:

  • ctl的计算以及相关的参数如下

    private static final int  AC_SHIFT   = 48;
    private static final long AC_MASK    = 0xffffL << AC_SHIFT;
    
    private static final int  TC_SHIFT   = 32;
    private static final long TC_MASK    = 0xffffL << TC_SHIFT;
    
    this.ctl = ((np << AC_SHIFT) & AC_MASK) | ((np << TC_SHIFT) & TC_MASK);
    
  • (np << AC_SHIFT) & AC_MASK则为:

  • (np << TC_SHIFT) & TC_MASK则为

  • 最终ctl的值如下,可以看出:刚初始化ctl时, AC = TC且其值均为负数;sp = 0,表示没有waiting worker

  • 与ctl有关的典型的位运算

    public int getPoolSize() {
    	// TC = 工作线程总数 - parallelism ----> 工作线程总数 = parallelism + TC
    	// TC位于ctl的48 ~ 33 bit,因此(short)(ctl >>> TC_SHIFT)可以计算TC
        return (config & SMASK) + (short)(ctl >>> TC_SHIFT);
    }
    
    public int getActiveThreadCount() {
        // AC = 活跃的工作线程数 - parallelism ----> 活跃的工作线程数 = parallelism + AC
        // AC位于ctl的64 ~ 49 bit,因此(int)(ctl >> AC_SHIFT)可以计算AC
        int r = (config & SMASK) + (int)(ctl >> AC_SHIFT);
        return (r <= 0) ? 0 : r; // suppress momentarily negative values
    }
    

4.1.3 成员变量的初始化

  • ForkJoinPool中,有很多的静态变量和成员变量,这些变量的初始化是分开进行的
    • 部分static final类型的变量,在声明时初始化。
    • 部分static final类型的变量,通过静态语句块初始化。这些变量与Unsafe机制有关,用于实现原子操作或位运算,是ForkJoinPool高效性能的一大保障
    • 部分成员变量,在private构造函数中初始化
    • 少部分成员变量,在使用时初始化。例如,存储工作队列的workQueues、借助U.compareAndSwapObject()进行初始化的stealCounter

4.1.4 ForkJoinPool自带的common池

  • ForkJoinPool还提供了一个static公共池,对应ForkJoinPool中的static final变量

    static final ForkJoinPool common;
    
  • common使用默认权限(包权限),为了让非java.util.concurrent包中的类也能使用公共池,ForkJoinPool提供了commonPool()方法,用于获取公共池

    public static ForkJoinPool commonPool() {
        // assert common != null : "static init error";
        return common;
    }
    
  • 任何未显式提交到指定池(如自定义的池)的ForkJoinTask,都将使用公共池进行处理。

  • 公共池中的线程,在不使用的时候可以被缓慢回收,而后续使用时又可以恢复,因此使用公共池可以减少资源开销

  • 公共池的初始化,是在静态语句块中完成的。其核心是调用makeCommonPool()方法,创建一个ForkJoinPool

    common = java.security.AccessController.doPrivileged
         (new java.security.PrivilegedAction<ForkJoinPool>() {
             public ForkJoinPool run() { return makeCommonPool(); }});
    
  • makeCommonPool()方法中,parallelism、threadFactory、exceptionHandler都可以通过系统属性进行设置,

    private static ForkJoinPool makeCommonPool() {
        int parallelism = -1;
        ForkJoinWorkerThreadFactory factory = null;
        UncaughtExceptionHandler handler = null;
        try {  // ignore exceptions in accessing/parsing properties
            String pp = System.getProperty
                ("java.util.concurrent.ForkJoinPool.common.parallelism");
            String fp = System.getProperty
                ("java.util.concurrent.ForkJoinPool.common.threadFactory");
            String hp = System.getProperty
                ("java.util.concurrent.ForkJoinPool.common.exceptionHandler");
            ... // 省略对系统属性设置的值的处理
        } catch (Exception ignore) { // 有异常也不做处理,不知是何用意
        }
        ... // 省略对并行度、factory、exceptionHandler的初始化
        // 未指定parallelism,默认使用Runtime.getRuntime().availableProcessors()
        return new ForkJoinPool(parallelism, factory, handler, LIFO_QUEUE,
                                "ForkJoinPool.commonPool-worker-");
    }
    
  • 公共池,甚至是其他创建好的ForkJoinPool,可能永远不会被使用。因此,为了最小化初始构建开销,ForkJoinPool创建完成后只是初始化了十几个成员变量

  • 剩余的成员变量,在第一次提交任务到ForkJoinPool时,由externalSubmit()方法负责进行初始化

  • 后面的学习,将围绕任务的提交、执行等进行展开,会重点介绍externalSubmit()方法

4.2 ForkJoinPool的状态(runState)

  • 不管是线程还是线程池,都有自己的状态

  • ThreadPoolExecutor的状态有5种:RUNNING、SHUTDOWN、STOP、TIDYING、TERMINATED,各状态之间的转换可以参考博客:线程池与线程的几种状态

  • ForkJoinPool的状态共有6种,由可加锁的runState变量记录

  • 其中,除了SHUTDOWN状态的值为负数,其他5种状态的值均为 2 n 2^n 2n

    volatile int runState;               // lockable status,线程池的状态
    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的二进制位与上述6种状态间的关系如下:

  • 上面的图,只是为了展示每种状态在runState中占据的位置,并非表示这些状态可以同时存在。如STARTED和STOP,二者互斥,不能同时存在

  • runState与状态有关的位运算如下

    // 或运算,设置状态
    rs | RSIGNAL
    
    // 与运算,判断状态;如果结果为0,说明不处于该状态
    rs & STARTED
    
    // 非运算 + 与运算,清除状态
    rs & ~RSLOCK
    

4.2.1 线程池的加锁与解锁(RSLOCK

  • 并发更新线程池的runState前,需要通过lockRunState()设置RSLOCK,从而为线程池加锁
    // 最终,通过CAS更新状态
    U.compareAndSwapInt(this, RUNSTATE, rs, ns = rs | RSLOCK)
    
  • 线程池加锁后,需要通过unlockRunState(int oldRunState, int newRunState)方法解锁,并将runState更新为newRunState
    • 其中,入参newRunState的RSLOCK已被清除。因此,当线程池的runState更新为newRunState时,线程池已解锁
    • 基于旧的runState,通过rs & ~RSLOCK清理RSLOCK;如果需要,还可以继续为runState设置其他的状态,就像下面的代码一样
      unlockRunState(rs, (rs & ~RSLOCK) | SHUTDOWN)
      

4.2.2 信号量RSIGNAL

  • RSIGNAL是一个信号量,线程池中的线程在进入阻塞状态前,会通过CAS操作设置RSIGNAL
    U.compareAndSwapInt(this, RUNSTATE, rs, rs | RSIGNAL)
    
  • 因此,(runState & RSIGNAL) != 0时,说明线程池中有线程需要被唤醒
  • 按照常理,线程被唤醒后,需要清除RSIGNAL。但在整个源码中,没有看到类似的代码:
    rs & ~RSIGNAL 
    
  • 后续,将以时序图的形式解释ForkJoinPool的大神作者,是如何偷偷清除RSIGNAL的

4.2.3 STARTED状态

  • 外部用户可以通过invoke()submit()execute()三种方法向ForkJoinPool提交任务,整个提交过程如下:

    同步任务
    有返回值的
    异步任务
    无返回值的
    异步任务
    无法处理
    的情况
    submitter
    invoke()
    submit()
    execute()
    externalPush()
    externalSubmit()
    完整版externalPush()
  • 向线程池提交第一个submission task时,externalSubmit()方法会对线程池进行 “二次” 初始化(主要是初始化之前尚未初始化的workQueuesstealCounter两个成员变量)

  • 初始化完成后,线程池将被置为STARTED状态。

  • 关键的初始化代码如下:

    U.compareAndSwapObject(this, STEALCOUNTER, null, new AtomicLong());
    workQueues = new WorkQueue[n];
    ns = STARTED;
    // 在finally中清除RSLOCK,并设置STARTED
    unlockRunState(rs, (rs & ~RSLOCK) | ns);
    

4.2.4 三种线程池关闭有关的状态

  • 调用shutdown()shutdownNow()方法关闭线程池,最终将调用tryTerminate(boolean now, boolean enable)方法
    public void shutdown() {
        checkPermission();
        tryTerminate(false, true);
    }
    public List<Runnable> shutdownNow() {
        checkPermission();
        tryTerminate(true, true);
        return Collections.emptyList();
    }
    
  • 不同的,shutdownNow()方法调用tryTerminate()方法时,对应的now参数为true,表示可以立即停止(STOP)线程池
  • tryTerminate()方法在终止线程池时,会先将线程池置为SHUTDOWN状态
  • 若tryTerminate()方法的参数now = true,在将线程池置为SHUTDOWN状态后,会将线程池置为STOP状态
  • 当线程池处于SHUTDOWN或STOP状态后,将不再接收新的任务
  • 线程池会等待已提交的任务执行完毕,或直接中断正在执行的任务、清理未执行的任务(结合源码 + 上述博客的猜测,欢迎交流
  • 最终,线程池中的工作线程数将为0,或者工作队列为空(null或长度为0)
  • 此时,tryTerminate()方法会将线程池置为TERMINATED状态

4.2.5 总结:运行状态的转化

  • 参照ThreadPoolExecutor的5种状态,可以简单地认为与ForkJoinPool运行有关的状态包括:STARTED、SHUTDOWN、STOP、TERMINATED
  • 笔者将这4种状态,叫做运行状态
  • 4种运行状态之间的转化,可以使用下图简单表示:

4.3 WorkQueue

  • 废话几句:
    • ForkJoinPool是目前为止,笔者见过的位运算最多的JDK源码😭
    • 同时,使用Unsafe类进行原子操作、park/unpark、值更新等,不仅让笔者见识到了Unsafe类丰富的操作,还给人一种ForkJoinPool的作者(大神Doug Lea)将Unsafe类里面所有的方法都使用了一遍的感觉
    • 大佬的能力💪,让我等菜鸟瑟瑟发抖😱
  • 本来讲完状态转换后,就应该以externalPush()方法为入口,深度剖析ForkJoinPool的运行机制
  • 但是,笔者发现,这时候应该先具备WorkQueue相关知识,才能更好地进行后续学习
  • 所以,在进入正题之前,还需要学习一下WorkQueue

4.3.1 成员变量

  • WorkQueue有两个静态成员变量,用于定义存储ForkJoinTask的array的初始容量和最大容量

  • array的容量必须为 2 n 2^n 2n,且至少为4

    静态变量描述
    int INITIAL_QUEUE_CAPACITY = 1 << 13;1. array的初始容量,2^13
    2. 由于JVM经常将数组放置到共享GC簿(如cardmarks),导致每次写入访问都会遇到严重的内存竞争。因此,这里将array的初始容量定义为一个较大值,2^13
    int MAXIMUM_QUEUE_CAPACITY = 1 << 26;1. array的最大容量, 2^26,64M
    2. 为什么是 2^26
        ① 为了确保索引计算不会wraparound(环绕),max_capacity <= 2^(31 - 4),即 2^27
        ② 为了帮助用户在系统饱和之前捕获失控程序,max_capacity应该比2^27略小,所以设置为2^26
  • WorkQueue的实例字段如下:

    // 由worker在池中的index(也就是WorkQueue在池中的索引)、status,加上一个version counter
    volatile int scanState;    
    // 存储pool stack中,前一个空闲worker的scanState
    int stackPred; 
    // worker在运行过程中,偷取的任务数           
    int nsteals;  
    // 使用随机数进行初始化,记录偷窃者在池中的索引,即workQueues的索引    
    int hint;                
    // 队列在池中的索引和队列的模式(LIFO_QUEUE/FIFO_QUEUE、SHARED_QUEUE)
    int config;            
    // 队列状态,1:加锁,0:未加锁,< 0: terminate
    volatile int qlock;   
      
    // 队列的base和top指针,用于指向array中的ForkJoinTask槽
    volatile int base;        
    int top;   
       
    // 队列中存储元素(ForkJoinTask)的数组,队列创建时并未初始化array       
    ForkJoinTask<?>[] array;
    // 队列所在的池,可能为null(待后续探究)
    final ForkJoinPool pool;  
    // 队列关联的工作线程,如果是shared queue,则为null
    final ForkJoinWorkerThread owner; 
    
    // 调用park()方法后,parker就是onwer;否则,parker为null
    volatile Thread parker;    
    // awaitJoin()中join到当前队列的任务
    volatile ForkJoinTask<?> currentJoin;  
    // 主要在helpStealer()方法中使用,记录worker当前窃取到的任务
    volatile ForkJoinTask<?> currentSteal; 
    

4.3.2 重要成员变量的解读

4.3.2.1 scanState
  • ForkJoinPool的状态叫做runState,WorkQueue的状态叫做scanState

  • scanState是一个32位的int值,由worker的状态(INACTIVESCANNING)、在池中的索引,加上一个版本计数器组成

  • 注意: scanState虽然是WorkQueue的一个状态,但是笔者更倾向于scanState是WorkQueue关联的worker的状态,从后续的源码学习便可以看出

  • INACTIVE1,表示worker是否处于非活动状态(可能由于等待信号而阻塞),无法执行任务

  • SCANNING1,表示worker正在扫描任务;为0,表示worker没有在扫描任务,换句话说,worker已经有任务在执行了,处于busy状态

  • 版本计数器,除了用作计数器,还用于版本标记(version stamp)以解决Treiber stack的ABA问题

  • 与scanState有关的mask和unit如下:

    static final int SCANNING     = 1;             // false when running tasks
    static final int INACTIVE     = 1 << 31;       // must be negative
    static final int SS_SEQ       = 1 << 16;       // version count
    
  • 可以看出:

    • SCANNING状态占据scanState的最低bit,worker处于SCANNING状态时,scanState的值为奇数
    • INACTIVE状态占据scanState的最高bit,worker处于INACTIVE状态时,scanState的值为负数
    • 版本计数器每次自增的步长为2^16
  • 不考虑worker在池中的索引,worker处于active、SCANNING状态时,如果其版本变化前后的值示例如下:

占据低16位的池索引

  • 从前面的学习可知,池中worker的最大数量为2^15;而worker对应处于奇数为的worker queue,加上处于偶数位的submission queue,池中队列的最大长度为2^16

  • 数组下标从0开始,所以池中队列长度的范围为 0 ~ 2 16 − 1 2^{16} - 1 2161,对应的二进制和16进制值为:

    0000 0000 0000 0000 ~ 1111 1111 1111 1111, 0x0000 ~ 0xffff
    
  • 不难看出,队列的长度可以使用16位的二进制进行表示,且worker在池中的索引是一个16位的奇数(末尾bit为1)

  • 按照源码,worker在池中的索引(简称池索引),将scanState占据其低16位

  • 问题来了: 池索引占据scanState的低16位,且末尾bit为1,而SCANNING状态也占据scanState的末尾bit,二者会发生冲突啊 😕

  • 若worker处于SCANNING状态,scanState最低bit为1,此时scanState的低16位可以存储池索引 —— 池索引和SCANNING状态无冲突

  • 若worker处于busy状态,scanState最低bit为0,就破坏了scanState低1位中存储的池索引

SCANNING状态与池索引如何共存?

  • 首先,需要明白INACTIVE和SCANNING所在的bit,可能存在哪些值的组合(浮于表面的描述,不一定正确)

    INACTIVESCANNING状态描述是否存在该组合
    10worker处于INACTIVE状态,却又正在执行任务不存在,worker既然不活跃,何来正在执行任务一说?
    11worker处于INACTIVE状态,正在扫描可执行的任务存在,但描述错误
    1. 因为worker处于INACTIVE状态,肯定不可能扫描或执行任务。
    2. 换句话说,worker处于INACTIVE状态时,SCANNING状态应该无效。
    3. 此时,scanState的低16位刚好可以用于存储池索引,而池索引的末尾bit为1,刚好与SCANNING状态为1对上,但这并不表示worker处于SCANNING状态
    00worker处于活跃状态,且正在执行任务存在,worker都在执行任务了,肯定是活跃状态
    此时,scanState低16位存储的池索引将被破坏
    01worker处于活跃状态,且正在扫描可以执行的任务存在,worker只有在活跃状态时,才可能扫描或执行任务
    同时,由于worker没有执行中的任务,scanState的低16位仍然可以存储池索引
  • 从上面的分析可以看出,worker的INACTIVE与SCANNING状态是相互制约的:

    • 处于INACTIVE状态时,SCANNING是无效的。这时,可以使用低16位存储池索引,所以会存在1 / 1的组合
    • 处于SCANNING状态时,INACTIVE状态是无效的
4.3.2.2 ctl的sp、scanState、stackPred之间的关系
  • 前面有提及:ctl的sp(sp = (int) ctl)记录的是空闲worker栈的栈顶worker的scanState

  • 除此之外,在整个Fork/Join框架中,sp、scanState、stackPred是构建空闲worker栈的关键,三者相互关联,其本源都是scanState

  • 下图,简单描述了新增空闲worker的流程

  • 新增空闲worker的流程,是参考如下代码绘制的:

    private ForkJoinTask<?> scan(WorkQueue w, int r) {
        WorkQueue[] ws; int m;
        if ((ws = workQueues) != null && (m = ws.length - 1) > 0 && w != null) {
            int ss = w.scanState;                     // initially non-negative
            for (int origin = r & m, k = origin, oldSum = 0, checkSum = 0;;) {
                ... // 省略其他代码,只展示关键代码
                if ((k = (k + 1) & m) == origin) {    // continue until stable
                    if ((ss >= 0 || (ss == (ss = w.scanState))) &&
                        oldSum == (oldSum = checkSum)) {
                        if (ss < 0 || w.qlock < 0)    // already inactive
                            break;
                        int ns = ss | INACTIVE;       // try to inactivate
                        long nc = ((SP_MASK & ns) |
                                   (UC_MASK & ((c = ctl) - AC_UNIT)));
                        w.stackPred = (int)c;         // hold prev stack top
                        U.putInt(w, QSCANSTATE, ns);
                        if (U.compareAndSwapLong(this, CTL, c, nc))
                            ss = ns;
                        else
                            w.scanState = ss;         // back out
                    }
                    checkSum = 0;
                }
            }
        }
        return null;
    }
    
  • 下图,简单描述从栈顶移除空闲worker,即唤醒栈顶空闲worker的流程:

  • 唤醒栈顶空闲worker,是参考如下代码绘制的:

    final void signalWork(WorkQueue[] ws, WorkQueue q) {
        long c; int sp, i; WorkQueue v; Thread p;
        while ((c = ctl) < 0L) {                       // too few active
            ... // 省略其他代码,只展示关键代码
            int vs = (sp + SS_SEQ) & ~INACTIVE;        // next scanState
            int d = sp - v.scanState;                  // screen CAS
            long nc = (UC_MASK & (c + AC_UNIT)) | (SP_MASK & v.stackPred);
            if (d == 0 && U.compareAndSwapLong(this, CTL, c, nc)) {
                v.scanState = vs;                      // activate v
                if ((p = v.parker) != null)
                    U.unpark(p);
                break;
            }
            ... // 省略其他代码,只展示关键代码 
        }
    }
    
  • 同时,从唤醒栈顶空闲worker可以看出:当worker从INACTIVE状态变成活跃状态,其scanState的版本计数器将增加1个步长(SS_SEQ

4.3.2.3 ForkJoinPool config vs WorkQueue config
  • ForkJoinPool和WorkQueue都有一个int类型的config变量,二者存储的内容却有差异,也有联系

    数值域ForkJoinPoolWorkQueue二者的联系
    高16位存储池中队列的asyncMode,true为FIFO_QUEUE,false为LIFO_QUEUE存储该队列的mode
    1. 若为worker queue,则对应LIFO_QUEUEFIFO_QUEUE两种值
    2. 若为submission queue,则对应SHARED_QUEUE
    创建worker queue时,其mode来自于ForkJoinPool的config,通过config & MODE_MASK计算得到
    低16位准确地说,是低15位,存储线程池的parallelism1. 存储该队列在池中的索引,也就也就workQueues的下标。
    2. 奇数索引为worker queue,偶数索引为submission queue
    1. workQueues数组的容量为parallelism最近2次幂的2倍,可以说,parallelism的值规定了队列索引的范围
    2. parallelism的最大值为 2 15 − 1 2^{15} - 1 2151,则workQueues数组的最大容量为 2 16 2^{16} 216,其索引范围为0x0000 ~ 0xffff。这也是为什么WorkQueue能使用config的低16位存储队列索引的原因

4.3.3 WorkQueue的初始化

4.3.3.1 构造函数
  • WorkQueue只有一个构造函数,入参在这里插入代码片:该队列所在的池,入参owner:与队列关联的worker(工作线程)
  • 初始化时,base和top指向array的中间,即1 << 12 = 2^12这个位置(因为array的初始容量为1 << 13
    WorkQueue(ForkJoinPool pool, ForkJoinWorkerThread owner) {
        this.pool = pool;
        this.owner = owner;
        // Place indices in the center of array (that is not yet allocated)
        base = top = INITIAL_QUEUE_CAPACITY >>> 1;
    }
    
  • 从构造函数可以看出,WorkQueue在创建时,很多字段都未进行初始化,如:非常重要的array字段
  • 不难猜测,同ForkJoinPool一样,WorkQueue也是出于某些资源初始化后可能并不会使用的原因,所以采用了懒初始化
4.3.3.2 array的初始化与扩容
  • 通过new WorkQueue()创建好队列后,在真正使用队列前,需要对之前未初始化的字段进行初始化,尤其是存储任务的array字段
  • array的初始化与扩容,都是用同一个方法growArray()
    final ForkJoinTask<?>[] growArray()
    
  • 如果是初始化array,则会创建一个长度为INITIAL_QUEUE_CAPACITYForkJoinTask<?>[]
  • 如果是扩容,则会在原来的基础上扩容2倍,即oldLength << 1
  • 若扩容操作后,array的容量大于MAXIMUM_QUEUE_CAPACITY,则会抛出RejectedExecutionException,以表示队列容量超过上限
  • 注意: resizing的过程中,可以移动base指针,但不能移动top指针

4.4 ForkJoinTask

  • 同ForkJoinPool、WorkQueue一样,ForkJoinTask也有自己的状态,叫做status

    // 包访问权限,可以被同一包的ForkJoinPool和ForkJoinWorkerThread直接访问
    volatile int status; 
    static final int DONE_MASK   = 0xf0000000;  // 屏蔽non-completion位,也就是屏蔽status的低24位
    static final int NORMAL      = 0xf0000000;  // 值为负数,表示任务正常结束
    static final int CANCELLED   = 0xc0000000;  // 小于NORMAL的负数,表示任务被取消
    static final int EXCEPTIONAL = 0x80000000;  // 小于CANCELLED的负数,表示任务因为异常而结束
    static final int SIGNAL      = 0x00010000;  // 整数,必须 >= 1 << 16
    static final int SMASK       = 0x0000ffff;  // status中tag掩码,tag位于status的低16位
    
  • status的二进制值构成如下:

  • 任务的几种completion状态都位于status的高4位,因此completion状态的掩码DONE_MASK0xf0000000

  • 任务状态比较好理解,SIGNAL和status中tag的作用还不清楚,若较重要,后续会补充


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值