Java併發指南7:JUC的核心類AQS詳解

一行一行源碼分析清楚AbstractQueuedSynchronizer


轉自https://www.javadoop.com/post/AbstractQueuedSynchronizer#toc4


在分析 Java 併發包 java.util.concurrent 源碼的時候,少不了需要了解 AbstractQueuedSynchronizer(以下簡寫AQS)這個抽象類,因爲它是 Java 併發包的基礎工具類,是實現 ReentrantLock、CountDownLatch、Semaphore、FutureTask 等類的基礎。

Google 一下 AbstractQueuedSynchronizer,我們可以找到很多關於 AQS 的介紹,但是很多都沒有介紹清楚,因爲大部分文章沒有把其中的一些關鍵的細節說清楚。

本文將從 ReentrantLock 的公平鎖源碼出發,分析下 AbstractQueuedSynchronizer 這個類是怎麼工作的,希望能給大家提供一些簡單的幫助。

申明以下幾點:

  1. 本文有點長,但是很簡單很簡單很簡單,主要面向讀者對象爲併發編程的初學者,或者想要閱讀java併發包源碼的開發者。

  2. 建議在電腦上閱讀,如果你想好好地理解所有的細節,而且你從來沒看過相關的分析,你可能至少需要 20 分鐘仔細看所有的描述,本文後面的 1/3 以上很簡單,前面的 1/4 更簡單,中間的部分要好好看。

  3. 如果你不知道爲什麼要看這個,我想告訴你,即使你看懂了所有的細節,你可能也不能把你的業務代碼寫得更好

  4. 源碼環境 JDK1.7,看到不懂或有疑惑的部分,最好能自己打開源碼看看。Doug Lea 大神的代碼寫得真心不錯。

  5. 有很多英文註釋我沒有刪除,這樣讀者可以參考着英文說的來,萬一被我忽悠了呢

  6. 本文不分析共享模式,這樣可以給讀者減少很多負擔,只要把獨佔模式看懂,共享模式讀者應該就可以順着代碼看懂了。而且也不分析 condition 部分,所以應該說很容易就可以看懂了。

  7. 本文大量使用我們平時用得最多的 ReentrantLock 的概念,本質上來說是不正確的,讀者應該清楚,AQS 不僅僅用來實現鎖,只是希望讀者可以用鎖來聯想 AQS 的使用場景,降低讀者的閱讀壓力

  8. ReentrantLock 的公平鎖和非公平鎖只有一點點區別,沒有任何閱讀壓力

  9. 你需要提前知道什麼是 CAS(CompareAndSet)

廢話結束,開始。

CLH隊列

此篇博客所有源碼均來自JDK 1.8

AQS內部維護着一個FIFO隊列,該隊列就是CLH同步隊列。

CLH同步隊列是一個FIFO雙向隊列,AQS依賴它來完成同步狀態的管理,當前線程如果獲取同步狀態失敗時,AQS則會將當前線程已經等待狀態等信息構造成一個節點(Node)並將其加入到CLH同步隊列,同時會阻塞當前線程,當同步狀態釋放時,會把首節點喚醒(公平鎖),使其再次嘗試獲取同步狀態。

在CLH同步隊列中,一個節點表示一個線程,它保存着線程的引用(thread)、狀態(waitStatus)、前驅節點(prev)、後繼節點(next),其定義如下:

static final class Node {
    /** 共享 */
    static final Node SHARED = new Node();

    /** 獨佔 */
    static final Node EXCLUSIVE = null;

    /**
     * 因爲超時或者中斷,節點會被設置爲取消狀態,被取消的節點時不會參與到競爭中的,他會一直保持取消狀態不會轉變爲其他狀態;
     */
    static final int CANCELLED =  1;

    /**
     * 後繼節點的線程處於等待狀態,而當前節點的線程如果釋放了同步狀態或者被取消,將會通知後繼節點,使後繼節點的線程得以運行
     */
    static final int SIGNAL    = -1;

    /**
     * 節點在等待隊列中,節點線程等待在Condition上,當其他線程對Condition調用了signal()後,改節點將會從等待隊列中轉移到同步隊列中,加入到同步狀態的獲取中
     */
    static final int CONDITION = -2;

    /**
     * 表示下一次共享式同步狀態獲取將會無條件地傳播下去
     */
    static final int PROPAGATE = -3;

    /** 等待狀態 */
    volatile int waitStatus;

    /** 前驅節點 */
    volatile Node prev;

    /** 後繼節點 */
    volatile Node next;

    /** 獲取同步狀態的線程 */
    volatile Thread thread;

    Node nextWaiter;

    final boolean isShared() {
        return nextWaiter == SHARED;
    }

    final Node predecessor() throws NullPointerException {
        Node p = prev;
        if (p == null)
            throw new NullPointerException();
        else
            return p;
    }

    Node() {
    }

    Node(Thread thread, Node mode) {
        this.nextWaiter = mode;
        this.thread = thread;
    }

    Node(Thread thread, int waitStatus) {
        this.waitStatus = waitStatus;
        this.thread = thread;
    }} final class Node {
    /** 共享 */
    static final Node SHARED = new Node();

    /** 獨佔 */
    static final Node EXCLUSIVE = null;

    /**
     * 因爲超時或者中斷,節點會被設置爲取消狀態,被取消的節點時不會參與到競爭中的,他會一直保持取消狀態不會轉變爲其他狀態;
     */
    static final int CANCELLED =  1;

    /**
     * 後繼節點的線程處於等待狀態,而當前節點的線程如果釋放了同步狀態或者被取消,將會通知後繼節點,使後繼節點的線程得以運行
     */
    static final int SIGNAL    = -1;

    /**
     * 節點在等待隊列中,節點線程等待在Condition上,當其他線程對Condition調用了signal()後,改節點將會從等待隊列中轉移到同步隊列中,加入到同步狀態的獲取中
     */
    static final int CONDITION = -2;

    /**
     * 表示下一次共享式同步狀態獲取將會無條件地傳播下去
     */
    static final int PROPAGATE = -3;

    /** 等待狀態 */
    volatile int waitStatus;

    /** 前驅節點 */
    volatile Node prev;

    /** 後繼節點 */
    volatile Node next;

    /** 獲取同步狀態的線程 */
    volatile Thread thread;

    Node nextWaiter;

    final boolean isShared() {
        return nextWaiter == SHARED;
    }

    final Node predecessor() throws NullPointerException {
        Node p = prev;
        if (p == null)
            throw new NullPointerException();
        else
            return p;
    }

    Node() {
    }

    Node(Thread thread, Node mode) {
        this.nextWaiter = mode;
        this.thread = thread;
    }

    Node(Thread thread, int waitStatus) {
        this.waitStatus = waitStatus;
        this.thread = thread;
    }}
 

CLH同步隊列結構圖如下:

201701240001_thumb-1.pnguploading.gif轉存失敗重新上傳取消201701240001

入列

學了數據結構的我們,CLH隊列入列是再簡單不過了,無非就是tail指向新節點、新節點的prev指向當前最後的節點,當前最後一個節點的next指向當前節點。代碼我們可以看看addWaiter(Node node)方法:

    private Node addWaiter(Node mode) {
        //新建Node
        Node node = new Node(Thread.currentThread(), mode);
        //快速嘗試添加尾節點
        Node pred = tail;
        if (pred != null) {
            node.prev = pred;
            //CAS設置尾節點
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
        //多次嘗試
        enq(node);
        return node;
    }private Node addWaiter(Node mode) {
        //新建Node
        Node node = new Node(Thread.currentThread(), mode);
        //快速嘗試添加尾節點
        Node pred = tail;
        if (pred != null) {
            node.prev = pred;
            //CAS設置尾節點
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
        //多次嘗試
        enq(node);
        return node;
    }

addWaiter(Node node)先通過快速嘗試設置尾節點,如果失敗,則調用enq(Node node)方法設置尾節點

    private Node enq(final Node node) {
        //多次嘗試,直到成功爲止
        for (;;) {
            Node t = tail;
            //tail不存在,設置爲首節點
            if (t == null) {
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {
                //設置爲尾節點
                node.prev = t;
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
    }private Node enq(final Node node) {
        //多次嘗試,直到成功爲止
        for (;;) {
            Node t = tail;
            //tail不存在,設置爲首節點
            if (t == null) {
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {
                //設置爲尾節點
                node.prev = t;
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
    }

在上面代碼中,兩個方法都是通過一個CAS方法compareAndSetTail(Node expect, Node update)來設置尾節點,該方法可以確保節點是線程安全添加的。在enq(Node node)方法中,AQS通過“死循環”的方式來保證節點可以正確添加,只有成功添加後,當前線程纔會從該方法返回,否則會一直執行下去。

過程圖如下:

1485225206860201701240002_thumb.pnguploading.gif轉存失敗重新上傳取消1485225206860201701240002

出列

CLH同步隊列遵循FIFO,首節點的線程釋放同步狀態後,將會喚醒它的後繼節點(next),而後繼節點將會在獲取同步狀態成功時將自己設置爲首節點,這個過程非常簡單,head執行該節點並斷開原首節點的next和當前節點的prev即可,注意在這個過程是不需要使用CAS來保證的,因爲只有一個線程能夠成功獲取到同步狀態。過程圖如下:

201701240003_thumb.pnguploading.gif轉存失敗重新上傳取消201701240003



AQS 結構

先來看看 AQS 有哪些屬性,搞清楚這些基本就知道 AQS 是什麼套路了,畢竟可以猜嘛!

// 頭結點,你直接把它當做 當前持有鎖的線程 可能是最好理解的private transient volatile Node head;// 阻塞的尾節點,每個新的節點進來,都插入到最後,也就形成了一個隱視的鏈表private transient volatile Node tail;// 這個是最重要的,不過也是最簡單的,代表當前鎖的狀態,0代表沒有被佔用,大於0代表有線程持有當前鎖// 之所以說大於0,而不是等於1,是因爲鎖可以重入嘛,每次重入都加上1private volatile int state;// 代表當前持有獨佔鎖的線程,舉個最重要的使用例子,因爲鎖可以重入// reentrantLock.lock()可以嵌套調用多次,所以每次用這個來判斷當前線程是否已經擁有了鎖// if (currentThread == getExclusiveOwnerThread()) {state++}private transient Thread exclusiveOwnerThread; //繼承自AbstractOwnableSynchronizer

怎麼樣,看樣子應該是很簡單的吧,畢竟也就四個屬性啊。

AbstractQueuedSynchronizer 的等待隊列示意如下所示,注意了,之後分析過程中所說的 queue,也就是阻塞隊列不包含 head,不包含 head,不包含 head。

aqs-0.pnguploading.gif轉存失敗重新上傳取消aqs-0

等待隊列中每個線程被包裝成一個 node,數據結構是鏈表,一起看看源碼吧:

static final class Node {    /** Marker to indicate a node is waiting in shared mode */
    // 標識節點當前在共享模式下
    static final Node SHARED = new Node();    /** Marker to indicate a node is waiting in exclusive mode */
    // 標識節點當前在獨佔模式下
    static final Node EXCLUSIVE = null;    // ======== 下面的幾個int常量是給waitStatus用的 ===========
    /** waitStatus value to indicate thread has cancelled */
    // 代碼此線程取消了爭搶這個鎖
    static final int CANCELLED =  1;    /** waitStatus value to indicate successor's thread needs unparking */
    // 官方的描述是,其表示當前node的後繼節點對應的線程需要被喚醒
    static final int SIGNAL    = -1;    /** waitStatus value to indicate thread is waiting on condition */
    // 本文不分析condition,所以略過吧,下一篇文章會介紹這個
    static final int CONDITION = -2;    /**
     * waitStatus value to indicate the next acquireShared should
     * unconditionally propagate
     */
    // 同樣的不分析,略過吧
    static final int PROPAGATE = -3;    // =====================================================

    // 取值爲上面的1、-1、-2、-3,或者0(以後會講到)
    // 這麼理解,暫時只需要知道如果這個值 大於0 代表此線程取消了等待,
    // 也許就是說半天搶不到鎖,不搶了,ReentrantLock是可以指定timeouot的。。。
    volatile int waitStatus;    // 前驅節點的引用
    volatile Node prev;    // 後繼節點的引用
    volatile Node next;    // 這個就是線程本尊
    volatile Thread thread;

}

Node 的數據結構其實也挺簡單的,就是 thread + waitStatus + pre + next 四個屬性而已,大家先要有這個概念在心裏。

上面的是基礎知識,後面會多次用到,心裏要時刻記着它們,心裏想着這個結構圖就可以了。下面,我們開始說 ReentrantLock 的公平鎖。多嘴一下,我說的阻塞隊列不包含 head 節點。

aqs-0.pnguploading.gif轉存失敗重新上傳取消aqs-0

首先,我們先看下 ReentrantLock 的使用方式。

// 我用個web開發中的service概念吧public class OrderService {    // 使用static,這樣每個線程拿到的是同一把鎖,當然,spring mvc中service默認就是單例,別糾結這個
    private static ReentrantLock reentrantLock = new ReentrantLock(true);    public void createOrder() {        // 比如我們同一時間,只允許一個線程創建訂單
        reentrantLock.lock();        // 通常,lock 之後緊跟着 try 語句
        try {            // 這塊代碼同一時間只能有一個線程進來(獲取到鎖的線程),
            // 其他的線程在lock()方法上阻塞,等待獲取到鎖,再進來
            // 執行代碼...
            // 執行代碼...
            // 執行代碼...
        } finally {            // 釋放鎖
            reentrantLock.unlock();
        }
    }
}

ReentrantLock 在內部用了內部類 Sync 來管理鎖,所以真正的獲取鎖和釋放鎖是由 Sync 的實現類來控制的。

abstract static class Sync extends AbstractQueuedSynchronizer {

}

Sync 有兩個實現,分別爲 NonfairSync(非公平鎖)和 FairSync(公平鎖),我們看 FairSync 部分。

public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}

線程搶鎖

很多人肯定開始嫌棄上面廢話太多了,下面跟着代碼走,我就不廢話了。

static final class FairSync extends Sync {    private static final long serialVersionUID = -3000897897090466540L;      // 爭鎖
    final void lock() {
        acquire(1);
    }      // 來自父類AQS,我直接貼過來這邊,下面分析的時候同樣會這樣做,不會給讀者帶來閱讀壓力
    // 我們看到,這個方法,如果tryAcquire(arg) 返回true, 也就結束了。
    // 否則,acquireQueued方法會將線程壓到隊列中
    public final void acquire(int arg) { // 此時 arg == 1
        // 首先調用tryAcquire(1)一下,名字上就知道,這個只是試一試
        // 因爲有可能直接就成功了呢,也就不需要進隊列排隊了,
        // 對於公平鎖的語義就是:本來就沒人持有鎖,根本沒必要進隊列等待(又是掛起,又是等待被喚醒的)
        if (!tryAcquire(arg) &&            // tryAcquire(arg)沒有成功,這個時候需要把當前線程掛起,放到阻塞隊列中。
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) {
              selfInterrupt();
        }
    }    /**
     * Fair version of tryAcquire.  Don't grant access unless
     * recursive call or no waiters or is first.
     */
    // 嘗試直接獲取鎖,返回值是boolean,代表是否獲取到鎖
    // 返回true:1.沒有線程在等待鎖;2.重入鎖,線程本來就持有鎖,也就可以理所當然可以直接獲取
    protected final boolean tryAcquire(int acquires) {        final Thread current = Thread.currentThread();        int c = getState();        // state == 0 此時此刻沒有線程持有鎖
        if (c == 0) {            // 雖然此時此刻鎖是可以用的,但是這是公平鎖,既然是公平,就得講究先來後到,
            // 看看有沒有別人在隊列中等了半天了
            if (!hasQueuedPredecessors() &&                // 如果沒有線程在等待,那就用CAS嘗試一下,成功了就獲取到鎖了,
                // 不成功的話,只能說明一個問題,就在剛剛幾乎同一時刻有個線程搶先了 =_=
                // 因爲剛剛還沒人的,我判斷過了




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