ForkJoin框架詳解 一張圖搞明白工作竊取(work-stealing)機制

1 ForkJoin框架 

1.1 ForkJoin框架

ForkJoinPool一種ExecutorService的實現,運行ForkJoinTask任務。ForkJoinPool區別於其它ExecutorService,主要是因爲它採用了一種工作竊取(work-stealing)的機制。所有被ForkJoinPool管理的線程嘗試竊取提交到池子裏的任務來執行,執行中又可產生子任務提交到池子中。

    ForkJoinPool維護了一個WorkQueue的數組(數組長度是2的整數次方,自動增長)。每個workQueue都有任務隊列(ForkJoinTask的數組),並且用base、top指向任務隊列隊尾和隊頭。work-stealing機制就是工作線程挨個掃描任務隊列,如果隊列不爲空則取隊尾的任務並執行。示意圖如下:

1.2 demo小程序

    創建一個包含2個worker線程的pool,main線程提交2個任務(task-1、task-2),觸發worker線程工作,task-1任務fork出4個子任務。main線程負責同步兩個worker線程的工作進度。demo小程序演示了worker-2竊取worker-1的子任務:

package com.focuse.jdkdemo.concurrent;

import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.ForkJoinTask;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.locks.LockSupport;

/**
 * @date :Created in 2020/2/16 上午11:10
 * @description:
 * @modified By:
 */
public class ForkJoinDemo {
    private static Thread worker1 = null;
    private static Thread worker2 = null;
    private static AtomicBoolean  worker1Park = new AtomicBoolean();
    private static AtomicBoolean  worker2Park = new AtomicBoolean();
    static {
        worker1Park.set(false);
        worker2Park.set(false);
    }

    public abstract static class ForkJoinTaskDemo extends ForkJoinTask {
        @Override
        public Object getRawResult() {
            return null;
        }

        @Override
        protected void setRawResult(Object value) {

        }
    }

    public static void setParkFlag() {
        if (Thread.currentThread().getName().equals("worker-1")) {
            //自旋設置, 保證與main線程同步
            while (!worker1Park.compareAndSet(false, true)) {

            }
        } else if (Thread.currentThread().getName().equals("worker-2")) {
            //自旋設置, 保證與main線程同步
            while (!worker2Park.compareAndSet(false, true)) {

            }
        }
    }

    private static Runnable task1 = new Runnable() {
        @Override
        public void run() {
            worker1 = Thread.currentThread();
            worker1.setName("worker-1");
            System.out.println(Thread.currentThread().getName() + " execute task1");
            //暫停並且設置暫停flag以便main結束自旋
            setParkFlag();
            LockSupport.park();
            ForkJoinTask task11 = new ForkJoinTaskDemo() {
                @Override
                protected boolean exec() {
                    System.out.println(Thread.currentThread().getName() + " execute task1-1");
                    //暫停並且設置暫停flag以便main結束自旋
                    setParkFlag();
                    LockSupport.park();
                    return true;
                }
            };
            task11.fork();
            ForkJoinTask task12 = new ForkJoinTaskDemo() {
                @Override
                protected boolean exec() {
                    System.out.println(Thread.currentThread().getName() + " execute task1-2");
                    //暫停並且設置暫停flag以便main結束自旋
                    setParkFlag();
                    LockSupport.park();
                    return true;
                }
            };
            task12.fork();
            ForkJoinTask task13 = new ForkJoinTaskDemo() {
                @Override
                protected boolean exec() {
                    System.out.println(Thread.currentThread().getName() + " execute task1-3");
                    //暫停並且設置暫停flag以便main結束自旋
                    setParkFlag();
                    LockSupport.park();
                    return true;
                }
            };
            task13.fork();
            ForkJoinTask task14 = new ForkJoinTaskDemo() {
                @Override
                protected boolean exec() {
                    System.out.println(Thread.currentThread().getName() + " execute task1-4");
                    //暫停並且設置暫停flag以便main結束自旋
                    setParkFlag();
                    LockSupport.park();
                    return true;
                }
            };
            task14.fork();
            //提交4個子任務後暫停
            setParkFlag();
            LockSupport.park();
        }
    };

    private static Runnable task2 = new Runnable() {
        @Override
        public void run() {
            worker2 = Thread.currentThread();
            worker2.setName("worker-2");
            System.out.println(Thread.currentThread().getName() + " execute task2");
            //暫停並且設置暫停flag以便main結束自旋
            setParkFlag();
            LockSupport.park();
        }
    };

    public static void main(String[] args) {
        //只用兩個線程,方便測試
        ForkJoinPool pool = new ForkJoinPool(2);
        //step-1、step-2 提交2各任務,期望worker-1 worker-2各執行一個任務
        System.out.println("step-1 step-2: 提交2個任務,觸發2個worker線程");
        pool.submit(task1);
        pool.submit(task2);

        while (!worker1Park.get()) {
        }
        //step-3  喚醒worker-1 產生4個任務 然後worker-1繼續暫停
        System.out.println("\n*******************************************");
        System.out.println("step-3 喚醒worker-1 產生4個子任務");
        LockSupport.unpark(worker1);
        //自旋設置, 保證與worker-1線程同步
        while (!worker1Park.compareAndSet(true, false)) {
        }


        while (!worker1Park.get() || !worker2Park.get()) {
        }
        //step-4 喚醒worker-1 worker-2
        System.out.println("\n*******************************************");
        System.out.println("step-4 喚醒worker-1、worker-2:worker-1彈出task1-4執行,worker-2竊取task1-1執行");
        LockSupport.unpark(worker1);
        LockSupport.unpark(worker2);
        //自旋設置, 保證與worker-1線程同步
        while (!worker1Park.compareAndSet(true, false)) {
        }
        //自旋設置, 保證與worker-2線程同步
        while (!worker2Park.compareAndSet(true, false)) {
        }

        while (!worker1Park.get() || !worker2Park.get()) {
        }
        //step-5 喚醒worker-1 worker-2
        System.out.println("\n*******************************************");
        System.out.println("step-5 喚醒worker-1、worker-2:worker-1彈出task1-3執行,worker-2竊取task1-2執行");
        LockSupport.unpark(worker1);
        LockSupport.unpark(worker2);
        //自旋設置, 保證與worker-1線程同步
        while (!worker1Park.compareAndSet(true, false)) {
        }
        //自旋設置, 保證與worker-2線程同步
        while (!worker2Park.compareAndSet(true, false)) {
        }


        while (!worker1Park.get() || !worker2Park.get()) {
        }
        //喚醒worker-1 worker-2 結束
        LockSupport.unpark(worker1);
        LockSupport.unpark(worker2);
        //自旋設置, 保證與worker-1線程同步
        while (!worker1Park.compareAndSet(true, false)) {
        }
        //自旋設置, 保證與worker-2線程同步
        while (!worker2Park.compareAndSet(true, false)) {
        }

    }
}

運行結果如下:

2 源碼解讀

    這一part圍繞線程池(ForkJoinPool)結構已經線程池的關鍵動作講一下過程,線程池的關鍵動作無非是提交任務、運行任務、獲取任務結果。不過,對於ForkJoinPool有還有任務fork動作。

    ForkJoinPool裏面大量用到bit運算。做一下簡短說明:計算機的運算以補碼計算。補碼怎麼來?正數的補碼就是原碼,最高位是符號位爲0;負數的補碼是其正數的補碼按位取反(包括符號位),最後加1。

2.1 ForkJoinPool 和 WorkQueue

    ForkJoinPool幾個主要的成員變量說明如下:

  • config 是創建ForkJoinPool的配置,int類型32bits,高16位表示pool的mode(FIFO或LIFO),低16位表示parallelism(並行度,默認大小可用處理器數java.lang.Runtime#availableProcessors);
  • ctl 是ForkJoinPool的主要控制字段,long類型64bits,ctl不同的bit位表示不同的含義;
    • 高16位(63~48)表示活躍的線程,值爲活躍線程數減去parallelism(補碼錶示),初始值是0-parallelism,工作線程激活則加1,去激活則減1。當累積加了parallelism時第63bit位翻轉爲0,則不允許再激活工作線程。
    • 第47~32位表示當前所有工作線程(包括未激活的),值爲所有工作線程數-parallelism(補碼錶示),創建線程則加1,終止線程則減1。當累積加了parallelism時第47位翻轉位0,則不允許再創建線程;
    • 第31~16位表示非激活線程鏈中top線程的版本計數和線程狀態,與第15~0位合起來看;
    • 第15~0位表示非激活線程鏈中top線程的本地WorkQueue在ForkJoinPool.workQueues數組中下標索引,第31~0位合起來的值實際是非激活線程鏈中top線程的本地WorkQueue.scanState
  • workQueues 是ForkJoinPool維護一個WorkQueue數組,奇數下標的WorkQueue關聯一個worker線程,偶數下標的WorkQueue用來接收外部提交的任務(非worker線程提交的任務);
  • factory 創建worker線程的工廠;

源碼如下(筆者添加了註釋):

public class ForkJoinPool extends AbstractExecutorService {
    ......
    //筆者注:runState的bit位:SHUTDOWN是負數(int的最高bit位爲1),其餘的是2的整數次方
    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;

    // Instance fields
    //筆者注:ctl是ForkJoinPool的控制字段。long是64位bit,
    //最高16位(63~48)表示活躍的線程,值爲活躍的線程-parallelism(補碼錶示),
    //第47~32位表示當前所有工作線程(包括未激活的),值爲所有工作線程數-parallelism(補碼錶示),
    //第31~16位表示waiters線程鏈中top線程的版本計數和線程狀態
    //第15~0位表示waiters線程鏈中top線程的本地WorkQueue在ForkJoinPool.workQueues的下標索引
    volatile long ctl;                   // main pool control
    volatile int runState;               // lockable status
    //筆者注:config高16位是mode(FIFO或者LIFO),低16位是parallelism
    final int config;                    // parallelism, mode
    //筆者注:防止index衝突的隨機數種子
    int indexSeed;                       // to generate worker index
    //筆者注:WorkQueue數組,奇數WorkQueue有worker線程,偶數WorkQueue接收外部提交任務
    volatile WorkQueue[] workQueues;     // main registry
    //工作線程的創建工廠
    final ForkJoinWorkerThreadFactory factory;
    final UncaughtExceptionHandler ueh;  // per-worker UEH
    final String workerNamePrefix;       // to create worker name string
    //工作竊取的計數
    volatile AtomicLong stealCounter;    // also used as sync monitor
    ......
}

WorkQueue的幾個重要成員變量說明如下:

  • scanState int類型32bits,各bit位含義如下:
    • 第31位表示線程狀態(1非激活),第30~16位表示版本計數;
    • 第0位表示worker線程是否在運行任務(1-scanning,0-busy),這裏有個小技巧在創建worker線程的WorkQueue時scanState的第15~0位初始化爲ForkJoinPool.workQueues的下標(worker線程的WorkQueue的下標是奇數),當worker線程運行任務時第0位設置0(busy),任務運行結束第0位又設置1(恢復爲奇數),所以scanState的第15~0又可以表示在ForkJoinPool.workQueues數組的下標索引
  • stackPred  當worker線程從激活變爲非激活時設置值,且值爲ForkJoinPool的ctl的低32位(實際是前一個非激活線程),這樣就形成了一個非激活線程鏈;
  • config 高16位是mode(FIFO或者LIFO),低16位是ForkJoinPool.workQueues的下標
  • base 任務隊列的隊尾,工作竊取就是竊取base指向的任務;
  • top 任務隊列的隊頭(指向空),下一個push的位置;
  • array 任務隊列;
  • pool 所屬的ForkJoinPool實例;
  • owner 所屬的worker線程,如果在ForkJoinPool.workQueues數組中下標是奇數,則不爲空。

源碼如下(筆者添加了註釋):

static final class WorkQueue {
    ......
    // Instance fields
    //筆者注:最高位爲1表示非激活,第30~16位版本計數,第0表示是否在運行任務(1-scanning,0-busy)
    //
    volatile int scanState;    // versioned, <0: inactive; odd:scanning
    //筆者注:實際是前一個非激活線程,這樣就形成了一個waiters線程鏈
    int stackPred;             // pool stack (ctl) predecessor
    int nsteals;               // number of steals
    int hint;                  // randomization and stealer index hint
    //高16位是mode(FIFO或LIFO),低16位是ForkJoinPool.workQueues的下標
    int config;                // pool index and mode
    volatile int qlock;        // 1: locked, < 0: terminate; else 0
    //筆者注:任務隊列的隊尾,工作竊取就是竊取base指向的任務
    volatile int base;         // index of next slot for poll
    //筆者注:任務隊列的隊頭(指向空),下一個push的位置
    int top;                   // index of next slot for push
    //筆者注:任務隊列
    ForkJoinTask<?>[] array;   // the elements (initially unallocated)
    //筆者注:所屬的ForkJoinPool實例
    final ForkJoinPool pool;   // the containing pool (may be null)
    //筆者注:所屬的worker線程,如果在ForkJoinPool.workQueues數組中的下標是奇數則不爲空
    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
    ......
}

2.2 提交任務

    ForkJoinPool作爲ExecutorService的一個實現類,有submit方法提交任務,直接貼源碼出來如下:

public class ForkJoinPool extends AbstractExecutorService {
    ......
    //提交任務
    public <T> ForkJoinTask<T> submit(ForkJoinTask<T> task) {
        if (task == null)
            throw new NullPointerException();
        externalPush(task);
        return task;
    }
    ......
}

   

submit方法判斷了一下任務是否爲null,然後直接調用externalPush,源碼如下:

public class ForkJoinPool extends AbstractExecutorService {
    ......
    /**
    **/
    final void externalPush(ForkJoinTask<?> task) {
        WorkQueue[] ws; WorkQueue q; int m;
        int r = ThreadLocalRandom.getProbe();
        int rs = runState;
        //筆者注:(1)計算一個偶數下標,如果該下標下WorkQueue不爲空則嘗試添加到該WorkQueue
        if ((ws = workQueues) != null && (m = (ws.length - 1)) >= 0 &&
            (q = ws[m & r & SQMASK]) != null && r != 0 && rs > 0 &&
            U.compareAndSwapInt(q, QLOCK, 0, 1)) {// 筆者注:(2)加鎖鎖住改WorkQueue
            ForkJoinTask<?>[] a; int am, n, s;
            //筆者注:WorkQueue的任務隊列不爲null且未滿 a.length - 1 > q.top - q.base
            if ((a = q.array) != null &&
                (am = a.length - 1) > (n = (s = q.top) - q.base)) {
                int j = ((am & s) << ASHIFT) + ABASE;
                U.putOrderedObject(a, j, task);
                U.putOrderedInt(q, QTOP, s + 1);
                //筆者注:釋放鎖
                U.putIntVolatile(q, QLOCK, 0);
                //筆者注:喚醒工作線程
                if (n <= 1)
                    signalWork(ws, q);
                return;
            }
            //筆者注:釋放鎖
            U.compareAndSwapInt(q, QLOCK, 1, 0);
        }
        externalSubmit(task);
    }
    ......
}

submit的任務是提交到偶數下標的workQueue中。函數externalPush先計算一個下標位置"m & r & SQMASK",這個位置是偶數下標,如何保證是偶數?"m & r & SQMASK"又是什麼含義?m是pool.workQueues的數組size - 1(即2的整數次方-1),跟m做&運算能保證下標不超過workQueues.size;  r是當前線程的局部變量,取這個值儘可能避免衝突;SQMASK是一個常量0x007e,第0bit位是0,這就保證了下標計算出來一定是偶數。

  • (1) 判斷對應下標的workQueues的元素不爲空且pool的狀態正常;
  • (2)加鎖 U.compareAndSwapInt(q, QLOCK, 0, 1)) ;
  • (3)如果對應WorkQueue中任務隊列已初始化(不等於null)且未滿,則加入該隊列;
  • (4)釋放鎖 U.compareAndSwapInt(q, QLOCK, 1, 0);
  • (5) 如果添加成功,喚醒工作線程 sinalWork;
  • (6)如果沒有成功添加,調用externalSubmit方法;

我們再看一下externalSubmit函數源碼如下:

public class ForkJoinPool extends AbstractExecutorService {
    ......
    private void externalSubmit(ForkJoinTask<?> task) {
        int r;                                    // initialize caller's probe
        if ((r = ThreadLocalRandom.getProbe()) == 0) {
            ThreadLocalRandom.localInit();
            r = ThreadLocalRandom.getProbe();
        }
        //筆者注:循環操作,每次循環的動作都非常小,符合條件就跳出來
        for (;;) {
            WorkQueue[] ws; WorkQueue q; int rs, m, k;
            boolean move = false;
            //筆者注: runState爲負數,則表示shutdown,終止線程池並跳出循環
            if ((rs = runState) < 0) {
                tryTerminate(false, false);     // help terminate
                throw new RejectedExecutionException();
            }
            //筆者注:pool未初始化,則初始化然後繼續循環
            else if ((rs & STARTED) == 0 ||     // initialize
                     ((ws = workQueues) == null || (m = ws.length - 1) < 0)) {
                int ns = 0;
                rs = lockRunState();
                try {
                    if ((rs & STARTED) == 0) {
                        U.compareAndSwapObject(this, STEALCOUNTER, null,
                                               new AtomicLong());
                      //筆者注:這裏保證workQueues是2的整數次方,方法巧妙,n經過一系列位運算
                       //變成了一個從第0位開始出現連續1的直到出現一個bit位是0,然後就一直是0,
                       //這樣最後再加1就變成了2的整數次方。
                        // create workQueues array with size a power of two
                        int p = config & SMASK; // ensure at least 2 slots
                        int n = (p > 1) ? p - 1 : 1;
                        n |= n >>> 1; n |= n >>> 2;  n |= n >>> 4;
                        n |= n >>> 8; n |= n >>> 16; n = (n + 1) << 1;
                        workQueues = new WorkQueue[n];
                        ns = STARTED;
                    }
                } finally {
                    unlockRunState(rs, (rs & ~RSLOCK) | ns);
                }
            }
            //筆者注:如果workQueue不爲空,則加鎖嘗試添加。添加成功喚醒工作線程並跳出,否則繼續循環
            else if ((q = ws[k = r & m & SQMASK]) != null) {
                if (q.qlock == 0 && U.compareAndSwapInt(q, QLOCK, 0, 1)) {
                    ForkJoinTask<?>[] a = q.array;
                    int s = q.top;
                    boolean submitted = false; // initial submission or resizing
                    try {                      // locked version of push
                        if ((a != null && a.length > s + 1 - q.base) ||
                            (a = q.growArray()) != null) {
                            int j = (((a.length - 1) & s) << ASHIFT) + ABASE;
                            U.putOrderedObject(a, j, task);
                            U.putOrderedInt(q, QTOP, s + 1);
                            submitted = true;
                        }
                    } finally {
                        U.compareAndSwapInt(q, QLOCK, 1, 0);
                    }
                    if (submitted) {
                        signalWork(ws, q);
                        return;
                    }
                }
                move = true;                   // move on failure
            }
            // 筆者注:如果pool未被鎖,則創建一個WorkQueue,注意k在前面一個elseif賦值了一個偶數
            else if (((rs = runState) & RSLOCK) == 0) { // create new queue
                q = new WorkQueue(this, null);
                q.hint = r;
                q.config = k | SHARED_QUEUE;
                q.scanState = INACTIVE;
                rs = lockRunState();           // publish index
                if (rs > 0 &&  (ws = workQueues) != null &&
                    k < ws.length && ws[k] == null)
                    ws[k] = q;                 // else terminated
                unlockRunState(rs, rs & ~RSLOCK);
            }
            else
                move = true;                   // move if busy
            //筆者注:重新獲取下標
            if (move)
                r = ThreadLocalRandom.advanceProbe(r);
        }
    }
    ......
}

這裏是一個循環,每次循環依次判斷條件,符合條件就跳出來:

  • (1)判斷runState是否爲負數,如果是負數,則表示shutdown,終止線程池並返回;
  • (2)否則繼續判斷,如果pool未初始化,則初始化之後然後開始另一次循環從(1)開始判斷;
  • (3)否則繼續判斷,計算一個偶數下標k,如果workQueues[k]不爲null,嘗試添加任務進去,成功則喚醒工作線程並返回;
  • (4)否則繼續判斷,如果pool未被其它線程鎖住,則創建一個WorkQueue賦值給workQueues[k],注意k在(3)中已經賦予了一個偶數,然後開始另一次循環從(1)開始判斷;
  •  (5)否則,開始另一次循環從(1)開始判斷;

submit任務添加完成,又1個疑問沒揭開:爲什麼判斷任務隊列(workQueue.array)未滿用a.length - 1 > q.top - q.base,感覺不需要-1? 不過這不影響閱讀,也不影響整體對ForkJoin的理解。先記錄下來,未來可能整明白。

    此外,ForkJoin框架的還可以通過ForkJoinTask.fork來添加任務,源碼如下:

public abstract class ForkJoinTask<V> implements Future<V>, Serializable {
    ......
    //如果是work線程調用fork則添加到work線程的本地隊列裏面,否則添加到commonPool池子裏面    
    public final ForkJoinTask<V> fork() {
        Thread t;
        if ((t = Thread.currentThread()) instanceof ForkJoinWorkerThread)
            ((ForkJoinWorkerThread)t).workQueue.push(this);
        else
            ForkJoinPool.common.externalPush(this);
        return this;
    }
    ......
}

2.3 運行任務

     這一節主要說明工作線程是怎麼啓動的?主要是添加任務時調用的signalWork:

public class ForkJoinPool extends AbstractExecutorService {
    ......
    final void signalWork(WorkQueue[] ws, WorkQueue q) {
        long c; int sp, i; WorkQueue v; Thread p;
        //筆者注:ctl的高16位是激活線程數-parallelism的反碼,最高位爲0表示激活線程還可以增加
        while ((c = ctl) < 0L) {                       // too few active
            //筆者注:ctl的第32位是非激活線程鏈的top線程,爲0表示沒有非激活線程
            if ((sp = (int)c) == 0) {                  // no idle workers
              //筆者注:ctl的47~32位是線程總數-parallelism的反碼,最高爲0表示總的線程還可以加
                if ((c & ADD_WORKER) != 0L)            // too few workers
                    tryAddWorker(c);
                break;
            }
            if (ws == null)                            // unstarted/terminated
                break;
            if (ws.length <= (i = sp & SMASK))         // terminated
                break;
            if ((v = ws[i]) == null)                   // terminating
                break;
            //筆者注:以下邏輯是獲取非激活線程鏈的top線程然後激活,再激活之前要將下一個非激活線程(stackPred表示)放到ctl裏面
            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;
            }
            if (q != null && q.base == q.top)          // no more work
                break;
        }
    }
    ......
}
  • 先根據pool的控制字段ctl判斷是否需要添加新的worker線程,如果需要添加並返回
  • 否則,找到非激活線程鏈的top線程並激活,主要在激活之前,先要將下一個非激活線程(stackPred指向)設置到ctl,使之成爲top線程

創建worker則是調用pool中的factory(ForkJoinWorkerThreadFactory)創建一個工作線程,並將其註冊到pool中,註冊過程如下:

public class ForkJoinPool extends AbstractExecutorService {
    ......
    final WorkQueue registerWorker(ForkJoinWorkerThread wt) {
        UncaughtExceptionHandler handler;
        wt.setDaemon(true);                           // configure thread
        if ((handler = ueh) != null)
            wt.setUncaughtExceptionHandler(handler);
        WorkQueue w = new WorkQueue(this, wt);
        int i = 0;                                    // assign a pool index
        int mode = config & MODE_MASK;
        int rs = lockRunState();
        try {
            WorkQueue[] ws; int n;                    // skip if no array
            if ((ws = workQueues) != null && (n = ws.length) > 0) {
                int s = indexSeed += SEED_INCREMENT;  // unlikely to collide
                int m = n - 1;
                //保證i是奇數 | 1
                i = ((s << 1) | 1) & m;               // odd-numbered indices
                if (ws[i] != null) {                  // collision
                    int probes = 0;                   // step by approx half n
                    int step = (n <= 4) ? 2 : ((n >>> 1) & EVENMASK) + 2;
                    while (ws[i = (i + step) & m] != null) {
                        if (++probes >= n) {
                            workQueues = ws = Arrays.copyOf(ws, n <<= 1);
                            m = n - 1;
                            probes = 0;
                        }
                    }
                }
                w.hint = s;                           // use as random seed
                w.config = i | mode;
                //筆者注:下標賦值scanState,因爲worker線程最大值0x7fff+1,workQueues的size是其2倍,所以下標最大值0xffff(16bits) 
                w.scanState = i;                      // publication fence
                ws[i] = w;
            }
        } finally {
            unlockRunState(rs, rs & ~RSLOCK);
        }
        wt.setName(workerNamePrefix.concat(Integer.toString(i >>> 1)));
        return w;
    }
    ......
}

註冊實際上就是創建一個WorkQueue對象維護起來,這裏不多介紹,就注意一點worker線程本地隊列一定是ForkJoinPool.workQueues的奇數下標的元素。“i = ((s << 1) | 1) & m” 中位運算"| 1"就保證i是奇數,後面遞增的step是2的整數次方,所以i = i + step還是奇數。同時,下標索引賦值給WorkQueue的scanState變量,因爲worker線程最大值0x7fff+1,workQueues的size是其2倍,所以下標最大值0xffff(16bits)。

    work線程創建完成直接start:

public class ForkJoinPool extends AbstractExecutorService {
    ......
    private boolean createWorker() {
        ForkJoinWorkerThreadFactory fac = factory;
        Throwable ex = null;
        ForkJoinWorkerThread wt = null;
        try {
            if (fac != null && (wt = fac.newThread(this)) != null) {
                //筆者注:啓動工作線程
                wt.start();
                return true;
            }
        } catch (Throwable rex) {
            ex = rex;
        }
        deregisterWorker(wt, ex);
        return false;
    }
    ......
}

線程啓動執行ForkJoinWorkerThread.run,然後又會調用ForkJoinPool.runWorker

public class ForkJoinWorkerThread extends Thread {
    ......
    public void run() {
        if (workQueue.array == null) { // only run once
            Throwable exception = null;
            try {
                onStart();
                pool.runWorker(workQueue);
            } catch (Throwable ex) {
                exception = ex;
            } finally {
                ......
            }
        }
    }
    ......
}


public class ForkJoinPool extends AbstractExecutorService {
    ......
    final void runWorker(WorkQueue w) {
        //筆者注:擴容w的任務隊列,此處第一次調用相當於初始化
        w.growArray();                   // allocate queue
        int seed = w.hint;               // initially holds randomization hint
        int r = (seed == 0) ? 1 : seed;  // avoid 0 for xorShift
        for (ForkJoinTask<?> t;;) {
            //筆者注:掃描竊取任務,如果竊取到則執行任務
            if ((t = scan(w, r)) != null)
                w.runTask(t);
            else if (!awaitWork(w, r))
                break;
            r ^= r << 13; r ^= r >>> 17; r ^= r << 5; // xorshift
        }
    }
    ......
}

runWorker是最頂層的循環,線程一直循環直到終止。runWorker調用scan方法,scan就是工作竊取的核心實現,源碼如下:

public class ForkJoinPool extends AbstractExecutorService {
    ......
    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;;) {
                WorkQueue q; ForkJoinTask<?>[] a; ForkJoinTask<?> t;
                int b, n; long c;
                if ((q = ws[k]) != null) {
                    if ((n = (b = q.base) - q.top) < 0 &&
                        (a = q.array) != null) {      // non-empty
                        long i = (((a.length - 1) & b) << ASHIFT) + ABASE;
                        if ((t = ((ForkJoinTask<?>)
                                  U.getObjectVolatile(a, i))) != null &&
                            q.base == b) {
                            if (ss >= 0) {
                   //筆者注:任務存在,競爭竊取任務,競爭成功返回竊取成果,如果隊列中任務還有剩,喚醒非激活線程
                                if (U.compareAndSwapObject(a, i, t, null)) {
                                    q.base = b + 1;
                                    if (n < -1)       // signal others
                                        signalWork(ws, q);
                                    return t;
                                }
                            }
                  //筆者注:如果當前線程 未激活,嘗試激活非激活鏈的top線程(如果oldSum不爲0則再掃描一次並且會將oldSum設置爲0)
                            else if (oldSum == 0 &&   // try to activate
                                     w.scanState < 0)
                                tryRelease(c = ctl, ws[m & (int)c], AC_UNIT);
                        }
                        if (ss < 0)                   // refresh
                            ss = w.scanState;
                        r ^= r << 1; r ^= r >>> 3; r ^= r << 10;
                        origin = k = r & m;           // move and rescan
                        oldSum = checkSum = 0;
                        continue;
                    }
                    checkSum += b;
                }
                //筆者注:又掃描了一圈
                if ((k = (k + 1) & m) == origin) {    // continue until stable
                //筆者注:如果checSum==oldSum 說明連續2圈掃描過程中各隊列的base都沒變(即沒有任務),
                //設置線程未激活。然後繼續掃描,如果第3圈掃描到任務,則嘗試激活線程,如果第3圈還            
                //沒掃到且checkSum又沒變,那就返回null,嘗試讓線程park。
                    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;
    }
    ......
}
  • (1)掃描到任務且線程是激活的,並且競爭竊取任務(其它線程也可能掃描到)。競爭成功返回竊取成果,競爭失敗繼續掃描;
  • (2)掃描到任務但線程是未激活的(可能此前連續2圈都沒掃描到任務),則嘗試release非激活線程鏈的top線程(如果oldSum==0,繼續掃描並將oldSum、checkSum設置爲0以便下次能夠嘗試release線程);
  • (3)如果連續掃描pool.workQueues 兩圈都沒有任務,oldSum==checkSum 說明連續兩圈各隊列的base值相同(沒有被竊取過)、且隊列中當前沒有任務,那麼設置線程未激活。繼續掃描第3圈:
    • 第3圈掃描到任務,就會進入(2) 因爲已經線程已經設置爲未激活了
    • 第3圈未掃描到任務,如果checkSum還是未變,跳出循環返回null,讓線程park;如果checkSum變了,繼續第4圈重複第3圈的過程(此時oldSum已經是第3圈的checkSum)。
    • 如果返回null,runWorker會嘗試park當前線程。

2.4 獲取結果

    ForkJoinTask是Future的實現類,所以get方法可以獲取結果。不過,作爲ForkJoin框架的任務,有自己獨有的方法-join()

public abstract class ForkJoinTask<V> implements Future<V>, Serializable {
    ......
    public final V join() {
        int s;
        if ((s = doJoin() & DONE_MASK) != NORMAL)
            reportException(s);
        return getRawResult();
    }

    
    private int doJoin() {
        int s; Thread t; ForkJoinWorkerThread wt; ForkJoinPool.WorkQueue w;
        return (s = status) < 0 ? s :
            ((t = Thread.currentThread()) instanceof ForkJoinWorkerThread) ?
            (w = (wt = (ForkJoinWorkerThread)t).workQueue).
            tryUnpush(this) && (s = doExec()) < 0 ? s :
            wt.pool.awaitJoin(w, this, 0L) :
            externalAwaitDone();
    }

private int externalAwaitDone() {
        int s = ((this instanceof CountedCompleter) ? // try helping
                 ForkJoinPool.common.externalHelpComplete(
                     (CountedCompleter<?>)this, 0) :
                 ForkJoinPool.common.tryExternalUnpush(this) ? doExec() : 0);
        if (s >= 0 && (s = status) >= 0) {
            boolean interrupted = false;
            do {
                //筆者注:設置當前任務的信號bit位,當任務結束時就會調用notifyAll
                if (U.compareAndSwapInt(this, STATUS, s, s | SIGNAL)) {
                    synchronized (this) {
                        if (status >= 0) {
                            try {
                                //等待該任務對象notify
                                wait(0L);
                            } catch (InterruptedException ie) {
                                interrupted = true;
                            }
                        }
                        else
                            notifyAll();
                    }
                }
            } while ((s = status) >= 0);
            if (interrupted)
                Thread.currentThread().interrupt();
        }
        return s;
    }


    private int setCompletion(int completion) {
        for (int s;;) {
            if ((s = status) < 0)
                return s;
            if (U.compareAndSwapInt(this, STATUS, s, s | completion)) {
                //筆者注:如果狀態的信號位(第17位)是1,則notifyAll喚醒所有等待結果的線程
                if ((s >>> 16) != 0)
                    synchronized (this) { notifyAll(); }
                return completion;
            }
        }
    }
    ......
}

    join方法最終調用externalAwaitDone方法,externalAwaitDone設置任務的信號位爲1,然後調用wait等待task對象喚醒。當任務完成時調用setCompletion方法。setCompletion判斷如果信號位爲1(說明有線程等待結果),則喚醒線程(notifyAll)。get方法和join方法差別不大,主要是對異常的處理和線程中斷的處理不一樣,此處不做贅述。

    好記性不如爛筆頭,隨時記錄當下的理解!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章