Fork/Join框架之雙端隊列

簡介

ForkJoinPool管理着ForkJoinWorkerThread線程,ForkJoinWorkerThread線程內部有一個雙端隊列,這個雙端隊列主要由一個數組queue、數組下標queueBase、數組上標queueTop三個值保證。

ForkJoinTask<?>[] queue:數組的大小必須是2的n次方,方便將取模轉換爲移位運算;

int queueTop:標識下一個被push或者pop的位置,這個值只會被當前線程修改,因些沒有加volatile修飾;

volatile int queueBase:下一個可以被其他線程steal的位置,由於其他線程會修改這個值,所以用volatile修飾保證可見性。


初始化

在線程的run方法啓動時,會調用線程的onStart()方法,在這個方法中對queue進行了初始化,長度爲1 << 13,這個方法並沒有對queueTop,queueBase進行賦值,採用默認值0。


擴容

當向線程中添加任務時,有可能會導致數組滿的情況,如下代碼所示:

final void pushTask(ForkJoinTask<?> t) {
    ForkJoinTask<?>[] q; int s, m;
    if ((q = queue) != null) {    // ignore if queue removed
        long u = (((s = queueTop) & (m = q.length - 1)) << ASHIFT) + ABASE;
        UNSAFE.putOrderedObject(q, u, t);
        queueTop = s + 1;         // or use putOrderedInt
        if ((s -= queueBase) <= 2)
            pool.signalWork();
        else if (s == m)
            growQueue();
    }
}

其中s代表queueTop的值,m爲數組長度-1,當s == m時,也就是queue數組中都放滿任務了,這時需要對數組進行擴容。

private void growQueue() {
    ForkJoinTask<?>[] oldQ = queue;
    int size = oldQ != null ? oldQ.length << 1 : INITIAL_QUEUE_CAPACITY;
    if (size > MAXIMUM_QUEUE_CAPACITY)
        throw new RejectedExecutionException("Queue capacity exceeded");
    if (size < INITIAL_QUEUE_CAPACITY)
        size = INITIAL_QUEUE_CAPACITY;
    ForkJoinTask<?>[] q = queue = new ForkJoinTask<?>[size];
    int mask = size - 1;
    int top = queueTop;
    int oldMask;
    if (oldQ != null && (oldMask = oldQ.length - 1) >= 0) {
        for (int b = queueBase; b != top; ++b) {
            long u = ((b & oldMask) << ASHIFT) + ABASE;
            Object x = UNSAFE.getObjectVolatile(oldQ, u);
            if (x != null && UNSAFE.compareAndSwapObject(oldQ, u, x, null))
                UNSAFE.putObjectVolatile
                    (q, ((b & mask) << ASHIFT) + ABASE, x);
        }
    }
}

從以上擴容代碼可以看出,最大容量不能超過MAXIMUM_QUEUE_CAPACITY(1 << 24),最小不能小於初始值。每次擴容爲先前大小的2倍,將原始數組複製到新數組中,同時將舊數組置null。擴容的過程中,queueBase和queueTop並不需要變化。


入隊列

向線程隊列中添加一個任務,或者向線程池添加一個任務時,如果這個任務是一個ForkJoinTask實例,就會做入隊列的操作。前面已有這段代碼,這裏簡要分析一下

long u = (((s = queueTop) & (m = q.length - 1)) << ASHIFT) + ABASE;
UNSAFE.putOrderedObject(q, u, t);
queueTop = s + 1;     
第一行,找到queueTop在數組中的位置

第二行,用新任務填充queueTop所在位置

第三行,queueTop加1.


出隊列

本地線程需要執行一個任務

final void execTask(ForkJoinTask<?> t) {
    currentSteal = t;
    for (;;) {
        if (t != null)
            t.doExec();
        if (queueTop == queueBase)
            break;
        t = locallyFifo ? locallyDeqTask() : popTask();
    }
    ++stealCount;
    currentSteal = null;
}
注意locallyFifo 這個屬性,是否對自己的隊列採用FIFO策略,默認爲false,即默認從queueTop一端取任務。如果這個值爲false,則從queueBase一端取數據。這個值可以通過ForkJoinPool類的asyncMode屬性加以修改。
final ForkJoinTask<?> locallyDeqTask() {
    ForkJoinTask<?> t; int m, b, i;
    ForkJoinTask<?>[] q = queue;
    if (q != null && (m = q.length - 1) >= 0) {
        while (queueTop != (b = queueBase)) {
            if ((t = q[i = m & b]) != null &&
                queueBase == b &&
                UNSAFE.compareAndSwapObject(q, (i << ASHIFT) + ABASE,
                                            t, null)) {
                queueBase = b + 1;
                return t;
            }
        }
    }
    return null;
}

private ForkJoinTask<?> popTask() {
    int m;
    ForkJoinTask<?>[] q = queue;
    if (q != null && (m = q.length - 1) >= 0) {
        for (int s; (s = queueTop) != queueBase;) {
            int i = m & --s;
            long u = (i << ASHIFT) + ABASE; // raw offset
            ForkJoinTask<?> t = q[i];
            if (t == null)   // lost to stealer
                break;
            if (UNSAFE.compareAndSwapObject(q, u, t, null)) {
                queueTop = s; // or putOrderedInt
                return t;
            }
        }
    }
    return null;
}
關鍵兩句話:

queueBase = b + 1:FIFO策略每次從queueBase取任務,每取一個,queueBase增加1;
--s,queueTop = s:LIFO策略每次從queueTop取任務,每取一個,queueTop減1。


其他線程需要偷一個任務執行

以下是work-stealing的核心代碼

for (;;) {
    ForkJoinTask<?>[] q; int b, i;
    if (joinMe.status < 0)
        break outer;
    if ((b = v.queueBase) == v.queueTop ||
        (q = v.queue) == null ||
        (i = (q.length-1) & b) < 0)
        break;                  // empty
    long u = (i << ASHIFT) + ABASE;
    ForkJoinTask<?> t = q[i];
    if (task.status < 0)
        break outer;            // stale
    if (t != null && v.queueBase == b &&
        UNSAFE.compareAndSwapObject(q, u, t, null)) {
        v.queueBase = b + 1;
        v.stealHint = poolIndex;
        ForkJoinTask<?> ps = currentSteal;
        currentSteal = t;
        t.doExec();
        currentSteal = ps;
        helped = true;
    }
}
1、瞄到第i個位置這個任務,i = (q.length-1) & b,i其實就是queueBase在數組中所在的位置;
2、將這個位置上的任務設置爲null,並增加queueBase的值,設置stealHint表示你的東西被我偷了;

3、保存先前的currentSteal值,設置currentSteal爲這個偷來的task,然後執行這個task,執行完後,恢復currentSteal的值。

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