【JUC】JDK1.8原始碼分析之CyclicBarrier(四)

leesf發表於2016-04-18

一、前言

  有了前面分析的基礎,現在,接著分析CyclicBarrier原始碼,CyclicBarrier類在進行多執行緒程式設計時使用很多,比如,你希望建立一組任務,它們並行執行工作,然後在進行下一個步驟之前等待,直至所有的任務都完成,和join很類似,下面,開始分析原始碼。

二、CyclicBarrier資料結構

  分析原始碼可以知道,CyclicBarrier底層是基於ReentrantLockAbstractQueuedSynchronizer來實現的,所以,CyclicBarrier的資料結構也依託於AQS的資料結構,在前面對AQS的分析中已經指出了其資料結構,在這裡不再累贅。

三、CyclicBarrier原始碼分析

  3.1 類的繼承關係

public class CyclicBarrier {}

  說明:可以看到CyclicBarrier沒有顯示繼承哪個父類或者實現哪個父介面,根據Java語言規定,可知其父類是Object。

  3.2 類的內部類

  CyclicBarrier類存在一個內部類Generation,每一次使用的CycBarrier可以當成Generation的例項,其原始碼如下

private static class Generation {
    boolean broken = false;
}

  說明:Generation類有一個屬性broken,用來表示當前屏障是否被損壞。

  3.3 類的屬性 

public class CyclicBarrier {
    
    /** The lock for guarding barrier entry */
    // 可重入鎖
    private final ReentrantLock lock = new ReentrantLock();
    /** Condition to wait on until tripped */
    // 條件佇列
    private final Condition trip = lock.newCondition();
    /** The number of parties */
    // 參與的執行緒數量
    private final int parties;
    /* The command to run when tripped */
    // 由最後一個進入 barrier 的執行緒執行的操作
    private final Runnable barrierCommand;
    /** The current generation */
    // 當前代
    private Generation generation = new Generation();
    // 正在等待進入屏障的執行緒數量
    private int count;
}
View Code

  說明:該屬性有一個為ReentrantLock物件,有一個為Condition物件,而Condition物件又是基於AQS的,所以,歸根到底,底層還是由AQS提供支援。

  3.4 類的建構函式

  1. CyclicBarrier(int, Runnable)型建構函式 

public CyclicBarrier(int parties, Runnable barrierAction) {
        // 參與的執行緒數量小於等於0,丟擲異常
        if (parties <= 0) throw new IllegalArgumentException();
        // 設定parties
        this.parties = parties;
        // 設定count
        this.count = parties;
        // 設定barrierCommand
        this.barrierCommand = barrierAction;
    }
View Code

  說明:該建構函式可以指定關聯該CyclicBarrier的執行緒數量,並且可以指定在所有執行緒都進入屏障後的執行動作,該執行動作由最後一個進行屏障的執行緒執行。

  2. CyclicBarrier(int)型建構函式 

public CyclicBarrier(int parties) {
        // 呼叫含有兩個引數的建構函式
        this(parties, null);
    }
View Code

  說明:該建構函式僅僅執行了關聯該CyclicBarrier的執行緒數量,沒有設定執行動作。

  3.5 核心函式分析

  1. dowait函式

  此函式為CyclicBarrier類的核心函式,CyclicBarrier類對外提供的await函式在底層都是呼叫該了doawait函式,其原始碼如下。

private int dowait(boolean timed, long nanos)
        throws InterruptedException, BrokenBarrierException,
               TimeoutException {
        // 儲存當前鎖
        final ReentrantLock lock = this.lock;
        // 鎖定
        lock.lock();
        try {
            // 儲存當前代
            final Generation g = generation;
            
            if (g.broken) // 屏障被破壞,丟擲異常
                throw new BrokenBarrierException();

            if (Thread.interrupted()) { // 執行緒被中斷
                // 損壞當前屏障,並且喚醒所有的執行緒,只有擁有鎖的時候才會呼叫
                breakBarrier();
                // 丟擲異常
                throw new InterruptedException();
            }
            
            // 減少正在等待進入屏障的執行緒數量
            int index = --count;
            if (index == 0) {  // 正在等待進入屏障的執行緒數量為0,所有執行緒都已經進入
                // 執行的動作標識
                boolean ranAction = false;
                try {
                    // 儲存執行動作
                    final Runnable command = barrierCommand;
                    if (command != null) // 動作不為空
                        // 執行
                        command.run();
                    // 設定ranAction狀態
                    ranAction = true;
                    // 進入下一代
                    nextGeneration();
                    return 0;
                } finally {
                    if (!ranAction) // 沒有執行的動作
                        // 損壞當前屏障
                        breakBarrier();
                }
            }

            // loop until tripped, broken, interrupted, or timed out
            // 無限迴圈
            for (;;) {
                try {
                    if (!timed) // 沒有設定等待時間
                        // 等待
                        trip.await(); 
                    else if (nanos > 0L) // 設定了等待時間,並且等待時間大於0
                        // 等待指定時長
                        nanos = trip.awaitNanos(nanos);
                } catch (InterruptedException ie) { 
                    if (g == generation && ! g.broken) { // 等於當前代並且屏障沒有被損壞
                        // 損壞當前屏障
                        breakBarrier();
                        // 丟擲異常
                        throw ie;
                    } else { // 不等於當前帶後者是屏障被損壞
                        // We're about to finish waiting even if we had not
                        // been interrupted, so this interrupt is deemed to
                        // "belong" to subsequent execution.
                        // 中斷當前執行緒
                        Thread.currentThread().interrupt();
                    }
                }

                if (g.broken) // 屏障被損壞,丟擲異常
                    throw new BrokenBarrierException();

                if (g != generation) // 不等於當前代
                    // 返回索引
                    return index;

                if (timed && nanos <= 0L) { // 設定了等待時間,並且等待時間小於0
                    // 損壞屏障
                    breakBarrier();
                    // 丟擲異常
                    throw new TimeoutException();
                }
            }
        } finally {
            // 釋放鎖
            lock.unlock();
        }
    }
View Code

  說明:dowait方法的邏輯會進行一系列的判斷,大致流程如下。

  2. nextGeneration函式 

  此函式在所有執行緒進入屏障後會被呼叫,即生成下一個版本,所有執行緒又可以重新進入到屏障中,其原始碼如下  

    private void nextGeneration() {
        // signal completion of last generation
        // 喚醒所有執行緒
        trip.signalAll();
        // set up next generation
        // 恢復正在等待進入屏障的執行緒數量
        count = parties;
        // 新生一代
        generation = new Generation();
    }
View Code

  在此函式中會呼叫AQS的signalAll方法,即喚醒所有等待執行緒。如果所有的執行緒都在等待此條件,則喚醒所有執行緒。其原始碼如下 

public final void signalAll() {
            if (!isHeldExclusively()) // 不被當前執行緒獨佔,丟擲異常
                throw new IllegalMonitorStateException();
            // 儲存condition佇列頭結點
            Node first = firstWaiter;
            if (first != null) // 頭結點不為空
                // 喚醒所有等待執行緒
                doSignalAll(first);
        }
View Code

  說明:此函式判斷頭結點是否為空,即條件佇列是否為空,然後會呼叫doSignalAll函式,doSignalAll函式原始碼如下  

private void doSignalAll(Node first) {
            // condition佇列的頭結點尾結點都設定為空
            lastWaiter = firstWaiter = null;
            // 迴圈
            do {
                // 獲取first結點的nextWaiter域結點
                Node next = first.nextWaiter;
                // 設定first結點的nextWaiter域為空
                first.nextWaiter = null;
                // 將first結點從condition佇列轉移到sync佇列
                transferForSignal(first);
                // 重新設定first
                first = next;
            } while (first != null);
        }
View Code

  說明:此函式會依次將條件佇列中的節點轉移到同步佇列中,會呼叫到transferForSignal函式,其原始碼如下

    final boolean transferForSignal(Node node) {
        /*
         * If cannot change waitStatus, the node has been cancelled.
         */
        if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
            return false;

        /*
         * Splice onto queue and try to set waitStatus of predecessor to
         * indicate that thread is (probably) waiting. If cancelled or
         * attempt to set waitStatus fails, wake up to resync (in which
         * case the waitStatus can be transiently and harmlessly wrong).
         */
        Node p = enq(node);
        int ws = p.waitStatus;
        if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
            LockSupport.unpark(node.thread);
        return true;
    }
View Code

  說明:此函式的作用就是將處於條件佇列中的節點轉移到同步佇列中,並設定結點的狀態資訊,其中會呼叫到enq函式,其原始碼如下。  

private Node enq(final Node node) {
        for (;;) { // 無限迴圈,確保結點能夠成功入佇列
            // 儲存尾結點
            Node t = tail;
            if (t == null) { // 尾結點為空,即還沒被初始化
                if (compareAndSetHead(new Node())) // 頭結點為空,並設定頭結點為新生成的結點
                    tail = head; // 頭結點與尾結點都指向同一個新生結點
            } else { // 尾結點不為空,即已經被初始化過
                // 將node結點的prev域連線到尾結點
                node.prev = t; 
                if (compareAndSetTail(t, node)) { // 比較結點t是否為尾結點,若是則將尾結點設定為node
                    // 設定尾結點的next域為node
                    t.next = node; 
                    return t; // 返回尾結點
                }
            }
        }
    }
View Code

  說明:此函式完成了結點插入同步佇列的過程,也很好理解。

  綜合上面的分析可知,newGeneration函式的主要方法的呼叫如下,之後會通過一個例子詳細講解。

  3. breakBarrier函式

  此函式的作用是損壞當前屏障,會喚醒所有在屏障中的執行緒。原始碼如下  

private void breakBarrier() {
        // 設定狀態
        generation.broken = true;
        // 恢復正在等待進入屏障的執行緒數量
        count = parties;
        // 喚醒所有執行緒
        trip.signalAll();
    }
View Code

  說明:可以看到,此函式也呼叫了AQS的signalAll函式,由signal函式提供支援。

四、示例

  下面通過一個例子來詳解CyclicBarrier的使用和內部工作機制,原始碼如下  

package com.hust.grid.leesf.cyclicbarrier;

import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
/**
 * 
 * @author leesf
 * @time   2016.4.16
 */
class MyThread extends Thread {
    private CyclicBarrier cb;
    public MyThread(String name, CyclicBarrier cb) {
        super(name);
        this.cb = cb;
    }
    
    public void run() {
        System.out.println(Thread.currentThread().getName() + " going to await");
        try {
            cb.await();
            System.out.println(Thread.currentThread().getName() + " continue");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
public class CyclicBarrierDemo {
    public static void main(String[] args) throws InterruptedException, BrokenBarrierException {
        CyclicBarrier cb = new CyclicBarrier(3, new Thread("barrierAction") {
            public void run() {
                System.out.println(Thread.currentThread().getName() + " barrier action");
                
            }
        });
        MyThread t1 = new MyThread("t1", cb);
        MyThread t2 = new MyThread("t2", cb);
        t1.start();
        t2.start();
        System.out.println(Thread.currentThread().getName() + " going to await");
        cb.await();
        System.out.println(Thread.currentThread().getName() + " continue");

    }
}

  執行結果(某一次): 

t1 going to await
main going to await
t2 going to await
t2 barrier action
t2 continue
t1 continue
main continue

  說明:根據結果可知,可能會存在如下的呼叫時序。

  說明:由上圖可知,假設t1執行緒的cb.await是在main執行緒的cb.barrierAction動作是由最後一個進入屏障的執行緒執行的。根據時序圖,進一步分析出其內部工作流程。

  ① main(主)執行緒執行cb.await操作,主要呼叫的函式如下。

  說明:由於ReentrantLock的預設採用非公平策略,所以在dowait函式中呼叫的是ReentrantLock.NonfairSync的lock函式,由於此時AQS的狀態是0,表示還沒有被任何執行緒佔用,故main執行緒可以佔用,之後在dowait中會呼叫trip.await函式,最終的結果是條件佇列中存放了一個包含main執行緒的結點,並且被禁止執行了,同時,main執行緒所擁有的資源也被釋放了,可以供其他執行緒獲取。

  ② t1執行緒執行cb.await操作,其中假設t1執行緒的lock.lock操作在main執行緒釋放了資源之後,則其主要呼叫的函式如下。

  說明:可以看到,之後condition queue(條件佇列)裡面有兩個節點,包含t1執行緒的結點插入在佇列的尾部,並且t1執行緒也被禁止了,因為執行了park操作,此時兩個執行緒都被禁止了。

  ③ t2執行緒執行cb.await操作,其中假設t2執行緒的lock.lock操作在t1執行緒釋放了資源之後,則其主要呼叫的函式如下。

  說明:由上圖可知,在t2執行緒執行await操作後,會直接執行command.run方法,不是重新開啟一個執行緒,而是最後進入屏障的執行緒執行。同時,會將Condition queue中的所有節點都轉移到Sync queue中,並且最後main執行緒會被unpark,可以繼續執行。main執行緒獲取cpu資源,繼續執行。

  ④ main執行緒獲取cpu資源,繼續執行,下圖給出了主要的方法呼叫。

  說明:其中,由於main執行緒是在AQS.CO的wait中被park的,所以恢復時,會繼續在該方法中執行。執行過後,t1執行緒被unpark,它獲得cpu資源可以繼續執行。

  ⑤ t1執行緒獲取cpu資源,繼續執行,下圖給出了主要的方法呼叫。

  說明:其中,由於t1執行緒是在AQS.CO的wait方法中被park,所以恢復時,會繼續在該方法中執行。執行過後,Sync queue中保持著一個空節點。頭結點與尾節點均指向它。

   注意:線上程await過程中中斷執行緒會丟擲異常,所有進入屏障的執行緒都將被釋放。至於CyclicBarrier的其他用法,讀者可以自行查閱API,不再累贅。

五、總結

  有了AQS與ReentrantLock的基礎,分析CyclicBarrier就會非常簡單,因為其底層就是由兩者支撐的,關於CycylicBarrier的原始碼就分析到此,有疑問的讀者,歡迎交流,謝謝各位園友的觀看~

  

 

相關文章