Java并發編程——ForkJoinPool之WorkQueue

一、ForkJoinPool

ForkJoinPool 是 JDK7 引入的,由 Doug Lea 編寫的高性能線程池。核心思想是將大的任務拆分成多個小任務(即fork),然后在將多個小任務處理匯總到一個結果上(即join),非常像MapReduce處理原理。同時,它提供基本的線程池功能,支持設置最大并發線程數,支持任務排隊,支持線程池停止,支持線程池使用情況監控,也是AbstractExecutorService的子類,主要引入了“工作竊取”機制,在多CPU計算機上處理性能更佳。其廣泛用在java8的stream中。

從圖中可以看出ForkJoinPool要先執行完子任務才能執行上一層任務,所以ForkJoinPool適合在有限的線程數下完成有父子關系的任務場景,比如:快速排序,二分查找,矩陣乘法,線性時間選擇等場景,以及數組和集合的運算。

Fork/Join Pool采用優良的設計、代碼實現和硬件原子操作機制等多種思路保證其執行性能。其中包括(但不限于):計算資源共享、高性能隊列、避免偽共享、工作竊取機制等。

二、與ThreadPoolExecutor原生線程池的區別

ForkJoinPool和ThreadPoolExecutor都實現了Executor和ExecutorService接口,都可以通過構造函數設置線程數,threadFactory,可以查看ForkJoinPool.makeCommonPool()方法的源碼查看通用線程池的構造細節。

在內部結構上我覺得兩個線程池最大的區別是在工作隊列的設計上,如下圖

ThreadPoolExecutor:

ForkJoinPool:

圖上細節畫的不嚴謹,但大致能看出區別:

  • ForkJoinPool每個線程都有自己的隊列
  • ThreadPoolExecutor共用一個隊列

使用ForkJoinPool可以在有限的線程數下來完成非常多的具有父子關系的任務,比如使用4個線程來完成超過2000萬個任務。

ForkJoinPool最適合計算密集型任務,而且最好是非阻塞任務,之前的一篇文章:Java踩坑記系列之線程池 也說了線程池的不同使用場景和注意事項。

所以ForkJoinPool是ThreadPoolExecutor線程池的一種補充,是對計算密集型場景的加強。

三、工作竊取的實現原理

ForkJoinPool類中的WorkQueue正是實現工作竊取的隊列,javadoc中的注釋如下:

大意是大多數操作都發生在工作竊取隊列中(在嵌套類工作隊列中)。這些是特殊形式的Deques,主要有push,pop,poll操作。

Deque是雙端隊列(double ended queue縮寫),頭部和尾部任何一端都可以進行插入,刪除,獲取的操作,即支持FIFO(隊列)也支持LIFO(棧)順序。

Deque接口的實現最常見的是LinkedList,除此還有ArrayDeque、ConcurrentLinkedDeque等。

工作竊取模式主要分以下幾個步驟:

  • 1、每個線程都有自己的雙端隊列。
  • 2、當調用fork方法時,將任務放進隊列頭部,線程以LIFO順序,使用push/pop方式處理隊列中的任務。
  • 3、如果自己隊列里的任務處理完后,會從其他線程維護的隊列尾部使用poll的方式竊取任務,以達到充分利用CPU資源的目的。
  • 4、從尾部竊取可以減少同原線程的競爭。
  • 5、當隊列中剩最后一個任務時,通過cas解決原線程和竊取線程的競爭。

流程大致如下所示:


工作竊取便是ForkJoinPool線程池的優勢所在,在一般的線程池比如ThreadPoolExecutor中,如果一個線程正在執行的任務由于某種原因無法繼續運行,那么該線程會處于等待狀態,包括singleThreadPool、fixedThreadPool、cachedThreadPool這幾種線程池。

而在ForkJoinPool中,那么線程會主動尋找其他尚未被執行的任務然后竊取過來執行,減少線程等待時間。

JDK8中的并行流(parallelStream)功能是基于ForkJoinPool實現的,另外還有java.util.concurrent.CompletableFuture異步回調future,內部使用的線程池也是ForkJoinPool。

四、ForkJoinPool分析

4.1 ForkJoinPool成員變量

// 用來配置ctl在控制線程數量使用
private static final long ADD_WORKER = 0x0001L << (TC_SHIFT + 15); // sign

//控制線程池數量(ctl & ADD_WORKER) != 0L 時創建線程,
// 也就是當ctl的第16位不為0時,可以繼續創建線程
volatile long ctl;                   // main pool control

//全局鎖控制,全局運行狀態
volatile int runState;               // lockable status

//config二進制形式的低16位表示parallelism,
//config二進制形式的第高16位表示mode,1表示異步模式, 使用先進先出隊列, 0表示同步模式, 使用先進后出棧
//低16位表示workerQueue在pool中的索引,高16位表示mode, 有FIFI LIFL 
final int config;  // parallelism, mode   
 
//生成workerQueue索引的重要依據
int indexSeed;         // to generate worker index  

//工作者隊列數組,內部線程ForkJoinWorkerThread啟動時會注冊一個WorkerQueue對象到這個數組中
volatile WorkQueue[] workQueues;     // main registry 

//工作者線程線程工廠,創建ForkJoinWorkerThread的策略
final ForkJoinWorkerThreadFactory factory;  

//在線程因未捕異常而退出時,java虛擬機將回調的異常處理策略
final UncaughtExceptionHandler ueh;  // per-worker UEH 

//工作者線程名的前綴
final String workerNamePrefix;       // to create worker name string  

//執行器所有線程竊取的任務總數,也作為監視runState的鎖
volatile AtomicLong stealCounter;    // also used as sync monitor

//通用的執行器,它在靜態塊中初始化
static final ForkJoinPool common; 

五、WorkQueue

5.1 類結構及其成員變量

5.1.1 類結構和注釋

WorkQueue是ForkJoinPool的核心內部類,是一個Contented修飾的靜態內部類。

/**
 * Queues supporting work-stealing as well as external task
 * submission. See above for descriptions and algorithms.
 * Performance on most platforms is very sensitive to placement of
 * instances of both WorkQueues and their arrays -- we absolutely
 * do not want multiple WorkQueue instances or multiple queue
 * arrays sharing cache lines. The @Contended annotation alerts
 * JVMs to try to keep instances apart.
 */
@sun.misc.Contended
static final class WorkQueue {
}

其注釋大意為:
workQUeue是一個支持任務竊取和外部提交任務的隊列,其實現參考ForkJoinPool描述的算法。在大多數平臺上的性能對工作隊列及其數組的實例都非常敏感。我們不希望多個工作隊列的實例和多個隊列數組共享緩存。@Contented注釋用來提醒jvm將workQueue在執行的時候與其他對象進行區別。

@Contented,實際上就是采用內存對齊的方式避免偽共享,保證WorkQueue在執行的時候,其前后不會有其他對象干擾。

注:JVM 添加 -XX:-RestrictContended 參數后 @sun.misc.Contended 注解才有效)

5.1.2 MAXIMUM_QUEUE_CAPACITY

MAXIMUM_QUEUE_CAPACITY注釋如下:

/**
 * Maximum size for queue arrays. Must be a power of two less
 * than or equal to 1 << (31 - width of array entry) to ensure
 * lack of wraparound of index calculations, but defined to a
 * value a bit less than this to help users trap runaway
 * programs before saturating systems.
 */
static final int MAXIMUM_QUEUE_CAPACITY = 1 << 26; // 64M

MAXIMUM_QUEUE_CAPACITY是隊列支持的最大容量,必須是2的冪小于或等于1<<(31-數組項的寬度),但定義為一個略小于此值的值,以幫助用戶在飽和系統之前捕獲失控的程序。

5.1.3 成員變量

成員變量區如下:

@sun.misc.Contended
static final class WorkQueue {

    //隊列的初始容量
    static final int INITIAL_QUEUE_CAPACITY = 1 << 13;

    // 64M 隊列的最大容量
    static final int MAXIMUM_QUEUE_CAPACITY = 1 << 26; // 64M

    // Instance fields
    volatile int scanState;    // versioned, <0: inactive; odd:scanning
    int stackPred;             // pool stack (ctl) predecessor
    int nsteals;               // number of steals
    int hint;                  // randomization and stealer index hint
    int config;                // pool index and mode
    volatile int qlock;        // 1: locked, < 0: terminate; else 0
    volatile int base;         // index of next slot for poll
    int top;                   // index of next slot for push
    ForkJoinTask<?>[] array;   // the elements (initially unallocated)
    final ForkJoinPool pool;   // the containing pool (may be null)
    final ForkJoinWorkerThread owner; // owning thread or null if shared
    volatile Thread parker;    // == owner during call to park; else null
    volatile ForkJoinTask<?> currentJoin;  // task being joined in awaitJoin
    volatile ForkJoinTask<?> currentSteal; // mainly used by helpStealer
}
  • scanState:它可以看作是樂觀鎖的版本號,另外它還有此其他功能,它為負數時,表示工作者線程非活動,它為奇數是表示,正在掃描(準備竊取)任務,它為偶數是表示正在執行任務。

  • stackPred:表示在線程池棧當前工作線程的前驅線程的索引。在喚醒線程時常用到此屬性。

  • nsteals:表示owner線程竊取的任務數。

  • hint:任務竊取時的隨機定位種子。

  • config:低16位表示,當前WorkerQueue對象在外部類的數組屬性workQueues中的索引(下標) 。高16位表示當前WorkerQueue對象的模式。對于內部任務,若構造方法配置為異步模式就將WorkQueue當作先進先出的隊列,反之將WorkQueue當作后進先出的棧。對于外部任務,將WorkQueue視為共享隊列。

  • qlock:初始值為0,”=1“時表示當前WorkerQueue對象被鎖住,” < 0“時 表示當前WorkerQueue對象已終止,隊列中的其他未完成任務將不再被執行。

  • base:表示下次對任務數組array進行poll出隊操作(竊取任務)的槽位索引(隊尾)。

  • top:表示下次任務數組array進行push入棧操作(添加任務)的槽位索引(棧頂)。

  • array:非學重要的屬性,這用是保存任務的數組(容器)。

  • pool:與之關聯的ForkJoinPool執行器,它可能為空。若為空,就使用靜態變量common作為執行器。

  • owner:當前隊列對應的工作者線程,它一般不為空。若從外部提交任務時,當前WorkerQueue對象表示共享隊列,owner為空。

  • parker:阻塞的線程。在被阻塞的時候,它等于owner,其他時候它為空。

  • currentJoin:表示當前正在join的任務,主要在awaitJoin方法使用。

  • currentSteal:表示當前被竊取的任務,主要在helpStealer方法中使用。

5.2 構造函數

WorkQueue就一個構造函數:

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;
}

在這個構造函數中,只會指定pool和owoner,如果該隊列是共享隊列,那么owoner此時是空的。此外,base和top兩個指針分別都指向了數組的中值,這個值是初始化容量右移一位。

那么結合前面的代碼,實際上初始化的時候,數組的長度為8192,那么base=top=4096。

這個數組在構造函數被調用之后初始化如下:


5.3 重要的方法

5.3.1 push

當ForkJoinWorkerThread需要向雙端隊列中放入一個新的待執行子任務時,會調用WorkQueue中的push方法。來看看這個方法的主要執行過程(請注意,源代碼來自JDK1.8,它和JDK1.7中的實現有顯著不同):

/**
 * Pushes a task. Call only by owner in unshared queues.  (The
 * shared-queue version is embedded in method externalPush.)
 *
 * @param task the task. Caller must ensure non-null.
 * @throws RejectedExecutionException if array cannot be resized
 */
final void push(ForkJoinTask<?> task) {
    ForkJoinTask<?>[] a; ForkJoinPool p;
    int b = base, s = top, n;
    // 請注意,在執行task.fork時,觸發push情況下,array不會為null
    // 因為在這之前workqueue中的array已經完成了初始化(在工作線程初始化時就完成了)    
    if ((a = array) != null) {    // ignore if queue removed
        //m為最高為位置的index
        int m = a.length - 1;     // fenced write for task visibility
        // U常量是java底層的sun.misc.Unsafe操作類
        // 這個類提供硬件級別的原子操作
        // putOrderedObject方法在指定的對象a中,指定的內存偏移量的位置,賦予一個新的元素      
        U.putOrderedObject(a, ((m & s) << ASHIFT) + ABASE, task);
        // putOrderedInt方法對當前指定的對象中的指定字段,進行賦值操作
        // 這里的代碼意義是將workQueue對象本身中的top標示的位置 + 1,    
        U.putOrderedInt(this, QTOP, s + 1);
        //如果n小于等于1則 且poll不為空 則觸發worker竊取或者產生新的worker
        if ((n = s - b) <= 1) {
            if ((p = pool) != null)
                // signalWork方法的意義在于,在當前活動的工作線程過少的情況下,創建新的工作線程
                p.signalWork(p.workQueues, this);
        }
        //如果n大于等于了m 則說明需要擴容了, array的剩余空間不夠了
        else if (n >= m)
            growArray();
    }
}

這個push方法是提供給工作隊列自己push任務來使用的,共享隊列push任務是在外部externalPush和externalSubmit等方法來進行初始化和push。

這里需要注意的是,當隊列中的任務數小于1的時候,才會調用signalWork,這個地方一開始并不理解,實際上,我們需要注意的是,這個方法是專門提供給工作隊列來使用的,那么這個條件滿足的時候,說明工作隊列空閑。如果這個條件不滿足,那么工作隊列中有很多任務需要工作隊列來處理,就不會觸發對這個隊列的竊取操作。

5.3.2 growArray

這是擴容的方法。實際上這個方法有兩個作用,首先是初始化,其次是判斷,是否需要擴容,如果需要擴容則容量加倍。

/**
 * Initializes or doubles the capacity of array. Call either
 * by owner or with lock held -- it is OK for base, but not
 * top, to move while resizings are in progress.
 */
final ForkJoinTask<?>[] growArray() {
    //舊的數組 oldA
    ForkJoinTask<?>[] oldA = array;
    //如果oldA不為空,則size就為oldA的長度*2,反之說明數組沒有被初始化,那么長度就應該為初始化的長度8192
    int size = oldA != null ? oldA.length << 1 : INITIAL_QUEUE_CAPACITY;
    //如果size比允許的最大容量還大,那么此時會拋出異常
    if (size > MAXIMUM_QUEUE_CAPACITY)
        throw new RejectedExecutionException("Queue capacity exceeded");
    int oldMask, t, b;
    //array a 為根據size new出來的一個新的數組
    ForkJoinTask<?>[] a = array = new ForkJoinTask<?>[size];
    //如果oldA不為空且其長度大于等于0為有效數組,且top-base大于0 說明不為空
    if (oldA != null && (oldMask = oldA.length - 1) >= 0 &&
        (t = top) - (b = base) > 0) {
        //按size定義掩碼
        int mask = size - 1;
        //從舊的數組中poll全部task,然后push到新的array中
        do { // emulate poll from old array, push to new array
            ForkJoinTask<?> x;
            //采用unsafe操作
            int oldj = ((b & oldMask) << ASHIFT) + ABASE;
            int j    = ((b &    mask) << ASHIFT) + ABASE;
            //實際上直接進行的內存對象copy,這樣效率比循環調用push和poll要高很多
            x = (ForkJoinTask<?>)U.getObjectVolatile(oldA, oldj);
            //判斷  x不為空 則使用unsafe進行操作
            if (x != null &&
                U.compareAndSwapObject(oldA, oldj, x, null))
                U.putObjectVolatile(a, j, x);
        } while (++b != t);
    }
    //返回新的數組
    return a;
}

需要注意的是,這個方法一旦調用進行擴容之后,無論是來自于外部push操作觸發,還是有工作線程worker觸發,都將被鎖定,之后,不能移動top指針,但是base指針是可以移動的。這也就是說,一旦處于擴容的過程中,就不能新增task,但是可以從base進行消費,這就只支持FIFO。因此同步模式將在此時被阻塞。

5.3.3 pop

同樣,pop操作也僅限于工作線程,對于共享對立中則不允許使用pop方法。這個方法將按LIFO后進先出的方式從隊列中。

/**
 * Takes next task, if one exists, in LIFO order.  Call only
 * by owner in unshared queues.
 */
final ForkJoinTask<?> pop() {
    ForkJoinTask<?>[] a; ForkJoinTask<?> t; int m;
    //如果array不為空切長度大于0
    if ((a = array) != null && (m = a.length - 1) >= 0) {
        //循環,s為top的指針減1,即top減1之后要大于0 也就是說要存在task
        for (int s; (s = top - 1) - base >= 0;) {
            //計算unsafe的偏移量 得到s的位置
            long j = ((m & s) << ASHIFT) + ABASE;
            //如果這個索引處的對象為空,則退出
            if ((t = (ForkJoinTask<?>)U.getObject(a, j)) == null)
                break;
            //反之用usafe的方法將這個值取走,之后返回,并更新top的指針
            if (U.compareAndSwapObject(a, j, t, null)) {
                U.putOrderedInt(this, QTOP, s);
                return t;
            }
        }
    }
    return null;
}

pop方法,這是僅限于owoner調用的方法,將從top指針處取出task。這個方法對于整個隊列是LIFO的方式。

5.3.4 poll

poll方法將從隊列中按FIFO的方式取出task。

/**
 * Takes next task, if one exists, in FIFO order.
 */
final ForkJoinTask<?> poll() {
    ForkJoinTask<?>[] a; int b; ForkJoinTask<?> t;
    //判斷 base-top小于0說明存在task 切array不為空
    while ((b = base) - top < 0 && (a = array) != null) {
        //計算出unsafe操作的索引 實際上就是拿到b
        int j = (((a.length - 1) & b) << ASHIFT) + ABASE;
        //之后拿到這個task 用volatile的方式
        t = (ForkJoinTask<?>)U.getObjectVolatile(a, j);
        //之后如果base和b相等
        if (base == b) {
           //如果拿到的task不為空
            if (t != null) {
                //那么將這個位置的元素移除 base+1 然后返回t
                if (U.compareAndSwapObject(a, j, t, null)) {
                    base = b + 1;
                    return t;
                }
            }
            //在上述操作之后,如果base比top小1說明已經為空了 直接退出循環
            else if (b + 1 == top) // now empty
                break;
        }
    }
    //默認返回null
    return null;
}

5.3.5 pollAt

這個方法將采用FIFO的方式,從 隊列中獲得task。

/**
 * Takes a task in FIFO order if b is base of queue and a task
 * can be claimed without contention. Specialized versions
 * appear in ForkJoinPool methods scan and helpStealer.
 */
final ForkJoinTask<?> pollAt(int b) {
    ForkJoinTask<?> t; ForkJoinTask<?>[] a;
    //數組不為空
    if ((a = array) != null) {
        //計算索引b的位置
        int j = (((a.length - 1) & b) << ASHIFT) + ABASE;
        //如果此處的task不為空,則將此處置為null然后將對象task返回
        if ((t = (ForkJoinTask<?>)U.getObjectVolatile(a, j)) != null &&
            base == b && U.compareAndSwapObject(a, j, t, null)) {
            base = b + 1;
            return t;
        }
    }
    return null;
}

通常情況下,b指的是隊列的base指針。那么從底部獲取元素就能實現FIFO。特殊的版本出現在scan和helpStealer中用于對工作隊列的竊取操作的實現。

5.3.6 nextLocalTask

/**
 * Takes next task, if one exists, in order specified by mode.
 */
final ForkJoinTask<?> nextLocalTask() {
    return (config & FIFO_QUEUE) == 0 ? pop() : poll();
}

這個方法中對之前的MODE會起作用,如果是FIFO則用pop方法,反之則用poll方法獲得下一個task。

5.3.7 peek

/**
 * Returns next task, if one exists, in order specified by mode.
 */
final ForkJoinTask<?> peek() {
    ForkJoinTask<?>[] a = array; int m;
    //判斷數組的合法性
    if (a == null || (m = a.length - 1) < 0)
        return null;
    //根據mode決定從top還是base處獲得task
    int i = (config & FIFO_QUEUE) == 0 ? top - 1 : base;
    int j = ((i & m) << ASHIFT) + ABASE;
    //返回獲得的task
    return (ForkJoinTask<?>)U.getObjectVolatile(a, j);
}

peek則根據之前的mode定義,從隊列的前面或者后面取得task。

5.3.8 tryUnpush

/**
 * Pops the given task only if it is at the current top.
 * (A shared version is available only via FJP.tryExternalUnpush)
*/
final boolean tryUnpush(ForkJoinTask<?> t) {
    ForkJoinTask<?>[] a; int s;
    //判斷數組的合法性
    if ((a = array) != null && (s = top) != base &&
        //將top位置的task與t比較,如果相等則將其改為null
        U.compareAndSwapObject
        (a, (((a.length - 1) & --s) << ASHIFT) + ABASE, t, null)) {
        //將top減1
        U.putOrderedInt(this, QTOP, s);
        //返回操作成功
        return true;
    }
    //默認返回失敗
    return false;
}

這個方法是將之前push的任務撤回。這個操作僅僅只有task位于top的時候操能成功。

5.3.9 runTask

在之前的文章分析外部提交task的時候,就提到了這個方法。實際上是runWorker調用的。
也就是說,線程在啟動之后,一旦worker獲取到task,就會運行。

/**
 * Executes the given task and any remaining local tasks.
 */
final void runTask(ForkJoinTask<?> task) {
    //task不為空
    if (task != null) {
        //掃描狀態標記為busy 那么說明當前的worker正在處理本地任務   此時這個操作會將scanState改為0
        scanState &= ~SCANNING; // mark as busy
        //執行這個task
        (currentSteal = task).doExec();
        //釋放已執行任務的內存
        U.putOrderedObject(this, QCURRENTSTEAL, null); // release for GC
        //執行其他本地的task
        execLocalTasks();
        ForkJoinWorkerThread thread = owner;
        //增加增加steals的次數
        if (++nsteals < 0)      // collect on overflow
            transferStealCount(pool);
        //將scanState改為1 這樣就變得活躍可以被其他worker scan
        scanState |= SCANNING;
        //如果thread不為null說明為worker線程 則調用后續的exec方法
        if (thread != null)
            thread.afterTopLevelExec();
    }
}

5.3.10 execLocalTasks

調用這個方法,運行隊列中的全部task,如果采用了LIFO模式,則調用pollAndExecAll,這是另外一種實現方法。直到將隊列都執行到empty

/**
 * Removes and executes all local tasks. If LIFO, invokes
 * pollAndExecAll. Otherwise implements a specialized pop loop
 * to exec until empty.
 */
final void execLocalTasks() {
    int b = base, m, s;
    //拿到數組
    ForkJoinTask<?>[] a = array;
    //如果b-s小于0說明存在task,a不為空,切a的長度大于0 這均是檢測方法的合法性
    if (b - (s = top - 1) <= 0 && a != null &&
        (m = a.length - 1) >= 0) {
        //如果沒有采用FIFO的mode  那么一定是LIFO 則從top處開始
        if ((config & FIFO_QUEUE) == 0) {
           //開始循環
            for (ForkJoinTask<?> t;;) {
               //從top開始取出task
                if ((t = (ForkJoinTask<?>)U.getAndSetObject
                     (a, ((m & s) << ASHIFT) + ABASE, null)) == null)
                    break;
                //修改top
                U.putOrderedInt(this, QTOP, s);
                //執行task
                t.doExec();
                //如果沒有任務的了 則退出
                if (base - (s = top - 1) > 0)
                    break;
            }
        }
        else
           //FIFO的方式調用pollAndExecAll
            pollAndExecAll();
    }
}

5.3.11 pollAndExecAll

此方法將用poll,FIFO的方式獲得task并執行。

final void pollAndExecAll() {
    for (ForkJoinTask<?> t; (t = poll()) != null;)
        t.doExec();
}

可見,當通過workQueue中調用runTask的方法的時候,會將這個隊列的scanState狀態修改為0,之后將這個隊列中的全部task根據定義的mode全部消費完畢。

5.3.12 tryRemoveAndExec

從注釋中可知,這個方法僅僅供awaitJoin方法調用,在await的過程中,將task從workQueue中移除并執行。

/**
 * If present, removes from queue and executes the given task,
 * or any other cancelled task. Used only by awaitJoin.
 *
 * @return true if queue empty and task not known to be done
 */
final boolean tryRemoveAndExec(ForkJoinTask<?> task) {
    ForkJoinTask<?>[] a; int m, s, b, n;
    //判斷數組的合法性 task不能為空
    if ((a = array) != null && (m = a.length - 1) >= 0 &&
        task != null) {
        //循環  n為task的數量,必須大于0
        while ((n = (s = top) - (b = base)) > 0) {
            //死循環 從top遍歷到base
            for (ForkJoinTask<?> t;;) {      // traverse from s to b
                long j = ((--s & m) << ASHIFT) + ABASE;
                if ((t = (ForkJoinTask<?>)U.getObject(a, j)) == null)
                    return s + 1 == top;     // shorter than expected
                //如果task處于top位置
                else if (t == task) {
                    boolean removed = false;
                    if (s + 1 == top) {      // pop
                        //pop的方式獲取task  然后替換為null
                        if (U.compareAndSwapObject(a, j, task, null)) {
                            U.putOrderedInt(this, QTOP, s);
                            removed = true;
                        }
                    }
                    //用emptytask代替
                    else if (base == b)      // replace with proxy
                        removed = U.compareAndSwapObject(
                            a, j, task, new EmptyTask());
                    //如果remove成功 則執行這個task
                    if (removed)
                        task.doExec();
                    break;
                }
                //如果task的status為負數 切 top=s=1
                else if (t.status < 0 && s + 1 == top) {
                    //移除
                    if (U.compareAndSwapObject(a, j, t, null))
                        U.putOrderedInt(this, QTOP, s);
                    break;                  // was cancelled
                }
                if (--n == 0)
                    return false;
            }
            if (task.status < 0)
                return false;
        }
    }
    return true;
}

5.3.13 popCC

如果pop CountedCompleter。這方法支持共享和worker的隊列,但是僅僅通過helpComplete調用。
CountedCompleter是jdk1.8中新增的一個ForkJoinTask的一個實現類。

/**
 * Pops task if in the same CC computation as the given task,
 * in either shared or owned mode. Used only by helpComplete.
 */
final CountedCompleter<?> popCC(CountedCompleter<?> task, int mode) {
    int s; ForkJoinTask<?>[] a; Object o;
    //判斷隊列數組合法性
    if (base - (s = top) < 0 && (a = array) != null) {
        //從top處開始
        long j = (((a.length - 1) & (s - 1)) << ASHIFT) + ABASE;
        //如果獲的的task不為null
        if ((o = U.getObjectVolatile(a, j)) != null &&
            //且為CountedCompleter對象
            (o instanceof CountedCompleter)) {
            //轉換為CountedCompleter
            CountedCompleter<?> t = (CountedCompleter<?>)o;
            //死循環
            for (CountedCompleter<?> r = t;;) {
                //如果task與獲得的r相等為同一對象
                if (r == task) {
                    //如果mode小于0 
                    if (mode < 0) { // must lock
                         //cas的方式加鎖
                        if (U.compareAndSwapInt(this, QLOCK, 0, 1)) {
                            //將這個對象清除 并修改top后解鎖
                            if (top == s && array == a &&
                                U.compareAndSwapObject(a, j, t, null)) {
                                U.putOrderedInt(this, QTOP, s - 1);
                                U.putOrderedInt(this, QLOCK, 0);
                                //返回t
                                return t;
                            }
                            //解鎖
                            U.compareAndSwapInt(this, QLOCK, 1, 0);
                        }
                    }
                    else if (U.compareAndSwapObject(a, j, t, null)) {
                        U.putOrderedInt(this, QTOP, s - 1);
                        return t;
                    }
                    break;
                }
                else if ((r = r.completer) == null) // try parent
                    break;
            }
        }
    }
    return null;
}

5.3.14 pollAndExecCC

pollAndExecCC 。竊取并運行與給定任務相同CountedCompleter計算任務(如果存在),并且可以在不發生爭用的情況下執行該任務。否則,返回一個校驗和/控制值,供helpComplete方法使用。

/**
 * Steals and runs a task in the same CC computation as the
 * given task if one exists and can be taken without
 * contention. Otherwise returns a checksum/control value for
 * use by method helpComplete.
 *
 * @return 1 if successful, 2 if retryable (lost to another
 * stealer), -1 if non-empty but no matching task found, else
 * the base index, forced negative.
 */
final int pollAndExecCC(CountedCompleter<?> task) {
    int b, h; ForkJoinTask<?>[] a; Object o;
    //判斷array的合法性
    if ((b = base) - top >= 0 || (a = array) == null)
        h = b | Integer.MIN_VALUE;  // to sense movement on re-poll
    else {
        //從base開始獲得task
        long j = (((a.length - 1) & b) << ASHIFT) + ABASE;
        if ((o = U.getObjectVolatile(a, j)) == null)
            h = 2;                  // retryable
        else if (!(o instanceof CountedCompleter))
            h = -1;                 // unmatchable
        else {
            CountedCompleter<?> t = (CountedCompleter<?>)o;
            //死循環
            for (CountedCompleter<?> r = t;;) {
                if (r == task) {
                    if (base == b &&
                        U.compareAndSwapObject(a, j, t, null)) {
                        base = b + 1;
                        t.doExec();
                        h = 1;      // success
                    }
                    else
                        h = 2;      // lost CAS
                    break;
                }
                else if ((r = r.completer) == null) {
                    h = -1;         // unmatched
                    break;
                }
            }
        }
    }
    return h;
}

externalPush方法中的“q = ws[m & r & SQMASK]”代碼非常重要。我們大致來分析一下作者的意圖,首先m是ForkJoinPool中的WorkQueue數組長度減1,例如當前WorkQueue數組大小為16,那么m的值就為15;r是一個線程獨立的隨機數生成器,關于java.util.concurrent.ThreadLocalRandom類的功能和使用方式可參見其它資料;而SQMASK是一個常量,值為126 (0x7e)。以下是一種可能的計算過程和計算結果:


實際上任何數和126進行“與”運算,其結果只可能是0或者偶數,即0、2、4、6、8。也就是說以上代碼中從名為“ws”的WorkQueue數組中,取出的元素只可能是第0個或者第偶數個隊列。

結論就是偶數是外部任務,奇數是需要拆解合并的任務。

ForkJoinWorkerThread需要從雙端隊列中取出下一個待執行子任務,就會根據設定的asyncMode調用雙端隊列的不同方法,代碼概要如下所示:

final ForkJoinTask<?> nextTaskFor(WorkQueue w) {
    for (ForkJoinTask<?> t;;) {
        WorkQueue q; int b;
        // 該方法試圖從“w”這個隊列獲取下一個待處理子任務
        if ((t = w.nextLocalTask()) != null)
            return t;
        // 如果沒有獲取到,則使用findNonEmptyStealQueue方法
        // 隨機得到一個元素非空,并且可以進行任務竊取的存在于ForkJoinPool中的其它隊列
        // 這個隊列被記為“q”   
        if ((q = findNonEmptyStealQueue()) == null)
            return null;
        // 試圖從“q”這個隊列base位處取出待執行任務  
        if ((b = q.base) - q.top < 0 && (t = q.pollAt(b)) != null)
            return t;
    }
}

六、總結

本文對workQueue的源碼進行了分析,我們需要注意的是,對于workQueue,定義了三個操作,分別是push,poll和pop。

  • push

主要是操作top指針,將top進行移動。

  • poll
    如果top和base不等,則說明隊列有值,可以消費,那么poll就從base指針處開始消費。這個方法實現了隊列的FIFO。

消費之后對base進行移動。

  • pop
    同樣,還可以從top開始消費,這就是pop。這個方法實際上實現了對隊列的LIFO。

消費之后將top減1。

以上就是這三個方法對應的操作。但是我們還需要注意的是,在所有的unsafe操作中,通過cas進行設置或者獲得task的時候,還有一個掩碼。這個非常重要。
我們可以看在push方法中:

 int m = a.length - 1;
 U.putOrderedObject(a, ((m & s) << ASHIFT) + ABASE, task);

在擴容的方法growArray中我們可以知道。每次擴容都是采用左移的方式來進行,這樣就保證了數組的長度為2的冪。

在這里,m=a.length-1,那就說明,m實際上其二進制格式將會有效位都為1,這個數字就可以做為掩碼。當m再與s取&計算的時候。可以想象,s大于m的部分將被去除,只會保留比m小的部分。那么實際上,這就等價于,當我們一直再push元素到數組中的時候,實際上就從數組的索引底部開始:


參考上面這個過程,也就是說,實際上這個數組,base和top實際指向的index并不重要。只有二者的相對位移才是重要的。這有點類似與RingBuffer的數據結構,但是還是有所不同。也就是說這個數組實際上是不會被浪費的。之前有很多不理解的地方,為什么top減去base可能出現負數。那么這樣實際上就會導致負數的產生。

這樣的話,如果我們采用異步模式,asyncMode為true的時候,workQueue則會采用FIFO_QUEUE的model,這樣workQueue本身就使用的時poll方法。反之如果使用LIFO_QUEUE的同步模式,則workQueue使用pop方法。默認情況下采用同步模式。同步的時候workQueue的指針都圍繞在數組的初始化的中間位置波動。而共享隊列則會一直循環。

至此,我們分析了workQueue的源碼,對其內部實現的雙端隊列本身的操作進行了分析。為什么作者會自己實現一個Deque,而不是使用juc中已存在的容器。這就是因為這個隊列全程都是采用Unsafe來實現的,在開篇作者也說了,需要@Contented修飾,就是為了避免緩存的偽代共享。這樣來實現一個高效的Deque,以供ForkJoinPool來操作。
這與學習ConcurrentHashMap等容器的源碼一樣,可以看出作者為了性能的優化,采用了很多獨特的方式來實現。這些地方都是我們值得學習和借鑒之處。這也是ForkJoin性能高效的關鍵。在作者的論文中也可以看出,java的實現,由于抽象在jvm之上,性能比c/c++的實現要低很多。這也是作者盡可能將性能做到最優的原因之一。

參考:
https://blog.csdn.net/Xiaowu_First/article/details/122407019

https://blog.csdn.net/tyrroo/article/details/81483608

https://www.cnblogs.com/juniorMa/articles/14234472.html

https://www.cnblogs.com/maoyx/p/13991828.html

https://blog.csdn.net/dhaibo1986/article/details/108801254

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,283評論 6 530
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 97,947評論 3 413
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,094評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,485評論 1 308
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,268評論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,817評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,906評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,039評論 0 285
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,551評論 1 331
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,502評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,662評論 1 366
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,188評論 5 356
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,907評論 3 345
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,304評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,563評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,255評論 3 389
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,637評論 2 370

推薦閱讀更多精彩內容