J.U.C|AQS共享式源碼分析

1、寫在前面


上篇給你們聊了獨佔式的源碼,具體參見《J.U.C|AQS獨佔式源碼分析》node

這一章咱們繼續在AQS的源碼世界中遨遊,解讀共享式同步狀態的獲取和釋放。segmentfault

2、什麼是共享式


共享式與獨佔式惟一的區別是在於同一時刻能夠有多個線程獲取到同步狀態。安全

咱們以讀寫鎖爲例來看二者,一個線程在對一個資源文件進行讀操做時,那麼這一時刻對於文件的寫操做均被阻塞,而其它線程的讀操做能夠同時進行。
當寫操做要求對資源獨佔操做,而讀操做能夠是共享的,兩種不一樣的操做對同一資源進行操做會是什麼樣的?看下圖併發

圖片描述
共享式訪問資源,其餘共享時均被容許,而獨佔式被阻塞。函數

圖片描述
獨佔式訪問資源時,其它訪問均被阻塞。oop

經過讀寫鎖給你們一塊兒溫故下獨佔式和共享式概念,上一節咱們已經聊過獨佔式,本章咱們主要聊共享式。源碼分析

主要講解方法ui

  • protected int tryAcquireShared(int arg);共享式獲取同步狀態,返回值 >= 0 表示獲取成功,反之則失敗。
  • protected boolean tryReleaseShared(int arg): 共享式釋放同步狀態。

3、核心方法分析


3.1 同步狀態的獲取

public final void acquireShared(int arg)this

共享式獲取同步狀態的頂級入口,若是當前線程未獲取到同步狀態,將會加入到同步隊列中等待,與獨佔式惟一的區別是在於同一時刻能夠有多個線程獲取到同步狀態。

方法源碼spa

public final void acquireShared(int arg) {
        if (tryAcquireShared(arg) < 0)
            doAcquireShared(arg);
    }

方法函數解析

  • tryAcquireShared(arg):獲取同步狀態,返回值大於等於0表示獲取成功,不然失敗。
  • doAcquireShared(arg):共享式獲取共享狀態,包含構建節點,加入隊列等待,喚醒節點等操做。

源碼分析

同步器的 acquireShared 和 doAcquireShared 方法

//請求共享鎖的入口
public final void acquireShared(int arg) {
        // 當state != 0 而且tryAcquireShared(arg) < 0 時纔去才獲取資源
        if (tryAcquireShared(arg) < 0)
            // 獲取鎖
            doAcquireShared(arg);
    }
// 以共享不可中斷模式獲取鎖
private void doAcquireShared(int arg) {
        // 將當前線程一共享方式構建成 node 節點並將其加入到同步隊列的尾部。這裏addWaiter(Node.SHARED)操做和獨佔式基本同樣,
        final Node node = addWaiter(Node.SHARED);
        // 是否成功標記
        boolean failed = true;
        try {
            // 等待過程是否被中斷標記
            boolean interrupted = false;
            自旋
            for (;;) {
                // 獲取當前節點的前驅節點
                final Node p = node.predecessor();
                // 判斷前驅節點是不是head節點,也就是看本身是否是老二節點
                if (p == head) {
                    // 若是本身是老二節點,嘗試獲取資源鎖,返回三種狀態
                    // state < 0 : 表示獲取資源失敗
                    // state = 0: 表示當前正好線程獲取到資源, 此時不須要進行向後繼節點傳播。
                    // state > 0: 表示當前線程獲取資源鎖後,還有多餘的資源,須要向後繼節點繼續傳播,獲取資源。 
                    int r = tryAcquireShared(arg);
                    // 獲取資源成功
                    if (r >= 0) {
                        // 當前節點線程獲取資源成功後,對後繼節點進行邏輯操做
                        setHeadAndPropagate(node, r);
                        // setHeadAndPropagate(node, r) 已經對node.prev = null,在這有對p.next = null; 等待GC進行垃圾收集。
                        p.next = null; // help GC
                        // 若是等待過程被中斷了, 將中斷給補上。
                        if (interrupted)
                            selfInterrupt();
                        failed = false;
                        return;
                    }
                }
                // 判斷狀態,尋找安全點,進入waiting狀態,等着被unpark()或interrupt()
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

在acquireShared(int arg)方法中,同步器調用tryAcquireShared(arg)方法獲取同步狀態,返回同步狀態有兩種。

當同步狀態大於等於0時: 表示能夠獲取到同步狀態,退出自旋,在doAcquireShared(int arg)方法中能夠看到節點獲取資源退出自旋的條件就是大於等於0

小於0會加入同步隊列中等待被喚醒。

addWaiter和enq方法

// 建立節點,並將節點加入到同步隊列尾部中。
 private Node addWaiter(Node mode) {
        // 以共享方式爲線程構建Node節點
        Node node = new Node(Thread.currentThread(), mode);
        // 嘗試快速加入到隊列尾部
        Node pred = tail;
        if (pred != null) {
            node.prev = pred;
            // CAS保證原子操做,將node節點加入到隊列尾部
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
        // 快速加入失敗,走 enq(node)方法
        enq(node);
        return node;
}
//以自旋的方式,將node節點加入到隊列的尾部
private Node enq(final Node node) {
        // 自旋
        for (;;) {
            // 獲取尾部節點
            Node t = tail;
            // 若是tail節點爲空, 說明同步隊列還沒初始化,必須先進行初始化
            if (t == null) { // Must initialize
                // CAS保證原子操做, 新建一個空 node 節點並將其設置爲head節點
                if (compareAndSetHead(new Node()))
                    // 設置成功並將tail也指向該節點
                    tail = head;
            } else {
                // 將node節點加入到隊列尾部
                node.prev = t;
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
    }

這兩個方法和獨佔式的基本相同,註釋中都標明瞭,在這就很少作解釋了。

獲取資源成功後對後繼節點的操做setHeadAndPropagate方法

private void setHeadAndPropagate(Node node, int propagate) {
        // 記錄老的head節點,以便覈對
        Node h = head; // Record old head for check below
        // 將node 設置成head節點
        setHead(node);
        // 這裏表示: 若是資源足夠(propagate > 0)或者舊頭節點爲空(h == null)或者舊節點的waitStatus爲 SIGNAL(-1) 或者 PROPAGATE(-3)(h.waitStatus < 0)
        // 或者當前head節點不爲空或者waitStatus爲SIGNAL(-1) 或者 PROPAGATE(-3),此時須要繼續喚醒後繼節點來嘗試獲取資源。
        if (propagate > 0 || h == null || h.waitStatus < 0 ||
            (h = head) == null || h.waitStatus < 0) {
            // 當前node節點的後繼節點
            Node s = node.next;
            //若是後節點爲空或者屬於共享節點
            if (s == null || s.isShared())
                // 繼續嘗試獲取資源
                doReleaseShared();
        }
    }

首先將當前節點設置爲head節點 setHead(node), 其次根據條件看是否對後繼節點繼續喚醒。

獲取資源失敗進行阻塞等待unpark

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        // 獲取前驅節點的等待狀態
        int ws = pred.waitStatus;
        // 若是等待狀態已經爲SIGNAL(表示當前當前節點的後繼節點處於等待狀態,若是當前節點釋放了同步狀態或者被中斷, 則會喚醒後繼節點)
        if (ws == Node.SIGNAL)
            // 直接返回,表示能夠安心的去休息了
            return true;
        // 若是前驅的節點的狀態 ws > 0(表示該節點已經被取消或者中斷,也就是成無效節點,須要從同步隊列中取消的)
        if (ws > 0) {
            // 循環往前需尋找,知道尋找到一個有效的安全點(一個等待狀態<= 0 的節點,排在它後面)
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            // 注意這一波操做後,獲獎取消的節點所有變成GC可回收的廢棄鏈。
            pred.next = node;
        } else {
            //若是前驅正常,那就把前驅的狀態設置成SIGNAL,告訴它獲取資源後通知本身一下。有可能失敗,人家說不定剛剛釋放完呢!
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }

 private final boolean parkAndCheckInterrupt() {
        // 調用park方法使當前節點的線程進入waiting
        LockSupport.park(this);    
        //返回線程中斷狀態
        return Thread.interrupted();
    }

這兩個方法和獨佔式基本相同。

接着看doReleaseShared 這個比較複雜

private void doReleaseShared() {
        //注意,這裏的頭結點已是上面新設定的頭結點了,從這裏能夠看出,若是propagate=0,
        //不會進入doReleaseShared方法裏面,那就有共享式變成了獨佔式
        for (;;) { // 死循環以防在執行此操做時添加新節點:退出條件 h == head
            Node h = head;
            // 前提條件,當前的頭節點不爲空,而且不是尾節點
            if (h != null && h != tail) {
                // 當前頭節點的等待狀態
                int ws = h.waitStatus;
                if (ws == Node.SIGNAL) {
                    // 若是當前節點的狀態爲SIGNAL,則利用CAS將其狀態設置爲0(也就是初始狀態)
                    //這裏不直接設爲Node.PROPAGATE,是由於unparkSuccessor(h)中,若是ws < 0會設置爲0,因此ws先設置爲0,再設置爲PROPAGATE
                    //這裏須要控制併發,由於入口有setHeadAndPropagate跟release兩個,避免兩次unpark
                    if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                        continue;            // loop to recheck cases 設置失敗,從新循環
                    // 喚醒後繼節點
                    unparkSuccessor(h);
                }
                // 若是等待狀態不爲0 則利用CAS將其狀態設置爲PROPAGATE ,以確保在釋放資源時可以繼續通知後繼節點。
                else if (ws == 0 &&
                         !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                    continue;                // loop on failed CAS
            }
            if (h == head)                   // loop if head changed 若是head 期間發生了改變,則須要重新循壞
                break;
        }
    }
private void unparkSuccessor(Node node) {
        
        int ws = node.waitStatus;
        // 在此再次判斷當前頭節點的的狀態,若是小於0 將設置爲0
        if (ws < 0)
            compareAndSetWaitStatus(node, ws, 0);
        //獲取後繼節點
        Node s = node.next;
        if (s == null || s.waitStatus > 0) {
            //若是後繼節點爲空或者等待狀態大於0 直接放棄。
            s = null;
            for (Node t = tail; t != null && t != node; t = t.prev)
                // 循環從尾部往前尋找下一個等待狀態不大於0的節點
                if (t.waitStatus <= 0)
                    s = t;
        }
        // 喚醒該節點的線程
        if (s != null)
            LockSupport.unpark(s.thread);
    }

3.2 共享狀態釋放
最後一步釋放資源就比較簡單了。

public final boolean releaseShared(int arg) {
        if (tryReleaseShared(arg)) {
            doReleaseShared();
            return true;
        }
        return false;
    }

4、總結


在獲取同步狀態時,同步器維護一個同步隊列,獲取狀態失敗的線程會加入到隊列中並進行自旋,出列的(或者中止自旋)的條件時前驅節點爲頭節點而且成功獲取了同步狀態。在釋放同步狀態時,調用Release方法釋放同步狀態,而後喚醒頭節點的後繼節點。

共享式方式在喚醒後繼節點得到資源後會判斷當前資源是否還有多餘的,若是有會繼續喚醒下一個節點。

相關文章
相關標籤/搜索