Java併發——AQS源碼深度解析

一. AQS是什麼?

  ASQ是AbstractQueuedSynchronizer的簡稱,它的類全限定名爲:java.util.concurrent.locks.AbstractQueuedSynchronizer. 後文中我們稱之爲同步器。
同步器是構建同步鎖和其他同步組件的基礎框架。它使用了一個int成員變量表示同步狀態,通過內部的FIFO隊列來完成資源的獲取和線程的排隊工作。
  同步器的主要使用方式是繼承。子類繼承同步器並實現它的抽象方式來管理同步狀態。在實現的過程中,主要就是修改成員變量state的值。
同步器中提供了3個方法來操作state:

方法名 說明
getState() 獲取變量state的當前值
setState(int newState) 設置state的值
compareAndSetState(int expect, int update) 通過CAS操作更新state的值

關於CAS操作網絡上有很多精闢的博客,這裏不再重複造輪子。

  要想理解同步器的工作原理,無非是徹底理解基於同步器中的鎖的特徵和獲取鎖與釋放鎖這兩個過程與各自的使用方式。比如鎖的模式有共享模式和獨佔模式;獲取鎖的方式有普通獲取、可中斷獲取、可自旋獲取等。

二.AQS內部結構

2.1 三個重要的屬性

同步器內部最重要的的三個屬性如下:

屬性名 類型 修飾符 解釋
state int volitile 同步狀態。AQS所有的操作都是圍繞着state變量展開的。
head Node volitile 指向FIFO隊列的頭部,除了初始化之外,只能通過setHead()方法設置。只有成功獲取鎖之後才能設置head
tail Node volitile 指向FIFO隊列的尾部。除了初始化之外,只有在獲取鎖失敗時,將線程構建成Node節點添加到FIFO隊列尾部並重新設置tail指向的節點。

2.2重要的內部類——Node

  同步器的FIFO隊列實際上是基於內部類Node構建的。
它的源碼如下:

static final class Node {
        // 表示Node爲共享模式
        static final Node SHARED = new Node();
        // 表示Node爲獨佔模式
        static final Node EXCLUSIVE = null;

        // ws狀態爲 1,表示當前線程執行了取消競爭鎖資源的操作。那麼這個線程所在的Node節點將會從FIFO隊列中剔除掉。
        // 另外,需要注意的是:ws狀態只要CANCELLED會大於0,即:wd>0成立的話,說明當前線程只有執行取消競爭鎖的操作這一種情況。
        static final int CANCELLED =  1;
        // wd狀態爲 -1,表示當前線程已經獲取到了鎖資源。
        static final int SIGNAL    = -1;
        // ws狀態爲-2,表示當前Node節點所在的線程獲取到了鎖,但是不滿足condition,
        // 這種情況下,需要將當前Node節點從FIFO隊列中移除,並添加到等待隊列中。
        static final int CONDITION = -2;
        // ws狀態爲-3,只有在共享模式下才會存在此值。表示鎖應該向下傳播。
        static final int PROPAGATE = -3;

        // waitStatus狀態就是上文中所說的ws,它的值只可能是以上5種情況外加0.
        // 當值爲0時表示什麼都不是。
        volatile int waitStatus;

        // 當前節點的前置節點
        volatile Node prev;

        // 當前節點的後繼節點
        volatile Node next;

        // 當前節點所代表的線程
        volatile Thread thread;

       // 當存在Condition時,此值表示在condition隊列下,此節點的後繼等待節點。
       // 當不存在Conditon時,此值如果指向SHARED,則表示此節點是共享模式,如果指向EXCLUSIVE,即nextWaiter == null,則表示此節點是獨佔模式。
        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() {    // Used to establish initial head or SHARED marker
        }

        // 構造方法,在addWaiter時調用此構造方法構建Node節點
        Node(Thread thread, Node mode) {     // Used by addWaiter
            this.nextWaiter = mode;
            this.thread = thread;
        }

        // 構造方法,在使用Condition時調用此構造方法構建Node節點
        Node(Thread thread, int waitStatus) { // Used by Condition
            this.waitStatus = waitStatus;
            this.thread = thread;
        }
    }

2.3 重要的內部類——ConditionObject

  由於ConditionObject的源碼太多,這裏我們只挑最重要的來說。

2.3.1 重要的屬性及說明

屬性名稱 類型 說明
firstWaiter Node 同一condition下,指向等待隊列的中的第一個Node
lastWaiter Node 同一condition下,指向等待隊列的中的最後一個Node

2.3.2 重要的方法及說明

方法名稱 說明
await() 將當前線程掛起,直到其他線程調用了signal()或者signalAll(),或者是當前線程被中斷
await(long time, TimeUnit unit) 和await方法一樣,只是在沒有被signal喚醒的話在經過time時間之後將自動醒來
awaitNanos(long nanosTimeout) 和await方法一樣,只是在沒有被signal喚醒的話,經過nanosTimeout時間之後將自動醒來
awaitUninterruptibly() 和await方法一樣,只是此方法會忽略掉線程中斷標誌
awaitUntil(Date deadline) 和await方法一樣,只是在沒有被signal喚醒的話,達到deadline時間之後將會自動醒來
signal() 將喚醒condition queue中的第一個Node節點(FIFO)
signalAll() 將喚醒condition queue中的所有的Node節點

2.4 AQS的隊列模型

  通過以上的介紹之後,在腦海裏就會浮現出同步器中的隊列模型了。不管是普通的隊列還是基於condition的條件隊列都是FIFO的。

2.4.1 普通的FIFO雙向隊列

AQS結構

  某個節點是共享模式時,如果這個節點所在的線程獲取到了鎖資源,鎖資源的獲取仍然會基於FIFO向後繼節點繼續傳播;
  反之,如果某個節點時獨佔模式時,如果這個節點所在的線程獲取到了鎖資源,則鎖資源將會被這個線程獨佔,直到其釋放了鎖資源之後,纔會喚醒後繼節點參與鎖資源的競爭。

2.4.2 基於condition的FIFO單向隊列

AQS結構-condition queue

2.4.3 FIFO隊列和Condition隊列如何互轉

  在使用condition時,必須先獲取到同步鎖,才能使用condition.await功能。這就意味着:當await時,線程將會從FIFO隊列中移除,然後基於Node(Thread thread, int waitStatus)構造方法構建成另一個Node節點添加到condition隊列的尾部。
  當調用signal()方法時,會將condition隊列的頭部即firstWaiter指向的Node節點移除並添加到FIFO隊列的尾部參與同步鎖的競爭。
  signalAll()方法本質上是和signal()一樣的,只是它會通過循環的方式將condition隊列上的所有節點都按照signal()的方式處理。

  另外,在一個同步器中,可以存在多個condition隊列!關於FIFO隊列以及基於condition的隊列我們在下面通過例子來進行說明。

三. AQS的使用示例

public class MyLock {

    private Sync sync = new Sync();

    public MyLock() {
        sync = new Sync();
    }
    
    // 加鎖,委託給內部類
    public void lock() {
        sync.acquire(1);
    }
    // 解鎖,委託給內部類
    public void release() {
        sync.release(1);
    }

    //通過靜態內部類繼承AQS,並實現抽象方法
    private static class Sync extends AbstractQueuedSynchronizer {
        @Override
        protected boolean tryAcquire(int arg) {
            if (compareAndSetState(0, 1)) {
                setExclusiveOwnerThread(Thread.currentThread());
                return true;
            }
            return false;
        }

        @Override
        protected boolean tryRelease(int arg) {
            if (getState() == 0) {
                throw new IllegalArgumentException();
            }
            if (compareAndSetState(1, 0)) {
                setExclusiveOwnerThread(null);
                return true;
            }
            return false;
        }
    }
}

  以上便是一個簡單的同步鎖的實現。接下來我們一步一步的來解析。

3.1 同步加鎖過程解析

  首先是通過調用acquire()方法來進行同步,它的源碼如下:

public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

  這個方法主要過程如下:

acquire方法

3.1.1 添加隊列源碼解析

private Node addWaiter(Node mode) {
        // 以當前線程構建節點
        Node node = new Node(Thread.currentThread(), mode);
        // 判斷尾節點是否存在,如果存在,則通過CAS操作嘗試快速將當前節點添加到隊列尾部,如果添加成功,則返回。
        Node pred = tail;
        if (pred != null) {
            node.prev = pred;
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
        // 如果尾部節點不存在或者快速添加失敗,則調用enq()方法添加到尾部。
        enq(node);
        return node;
    }

  1.首先構建以當前線程和模式構建一個Node節點;AQS有兩種模式:共享模式和獨佔模式。
  2.在隊列不爲空的情況下,通過CAS操作將當前節點添加到隊列的尾部。tail是同步器的一個變量,它指向尾節點。
  3.在隊列爲空的情況下,調用enq()方法添加節點到隊列中。enq()源碼如下:

private Node enq(final Node node) {
        // 自旋
        for (;;) {
            Node t = tail;
            // 如果隊列爲空,則通過CAS操作設置head節點,目的是爲了初始化隊列。
            // 如果隊列不爲空,則通過CAS操作嘗試將當前節點添加到隊列尾部。
            if (t == null) { // Must initialize
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {
                node.prev = t;
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
    }

  enq方法的主要目的是確保隊列能夠初始化成功,並且就當前節點添加到節點中。如何確保操作成功?自旋!如何確保線程安全?CAS操作

3.1.2 獲取同步鎖源碼解析

  4.當前線程添加到隊列之後, 通過acquireQueued()方法嘗試獲取鎖。它的源碼如下:

final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                // 獲取當前節點的前置節點
                final Node p = node.predecessor();
                // 如果head節點指向前置節點並且獲取鎖成功,則重新設置頭結點爲當前節點,並且將前置節點移除隊列。
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                // 如果獲取鎖失敗,則判斷是否需要將當前節點的線程掛起
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

  4.1 首先獲取當前節點的前置節點p,如果p就是隊列中的頭結點(即head指向的節點),則調用子類的實現的tryAcquire方法嘗試獲取鎖;
  4.2 如果獲取失敗,則判斷當前節點所擁有的是否需要park(掛起),判斷是否需要掛起的源碼解讀如下。

3.1.3 判斷當前線程是否需要掛起源碼解析

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        // 獲取前置節點的等待狀態即ws
        int ws = pred.waitStatus;
        // 如果前置節點ws爲SINGAL狀態,表示其已經獲取鎖並處於運行狀態,此時返回true
        // 把當前節點掛起等待;等待前置節點運行完畢釋放鎖並將當前節點喚醒。
        if (ws == Node.SIGNAL) {
            return true;
        }
        // 如果前置節點的等待狀態>0,表示前置節點已經取消了獲取鎖的競爭,
        // 此時需要將前置節點移除隊列。
        // 如果前置節點不是取消狀態,則嘗試CAS操作嘗試將當前節點的ws狀態設置爲SIGNAL狀態。如果設置成功,說明前置節點已經獲取到鎖並處於運行狀態咯!
        if (ws > 0) {
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        // 在前置節點不是SIGNAL狀態下,此時都會返回false,表示當前節點將繼續自旋來嘗試獲取鎖。
        return false;
    }

  4.2.1 首先檢查前置節點的ws狀態是否爲SIGNAL,如果是SIGNAL狀態,表示前置節點已經獲取鎖並處於運行中,當前節點的線程處於等待運行狀態,因此,返回true;
  4.2.2 否則,如果ws的值 > 0 即CANCEL狀態,說明前置節點已經取消了參與鎖的競爭,此時需要將這個前置節點從FIFO隊列中剔除!
  4.2.3 如果前置節點沒有CANCEL,則通過CAS機制嘗試將前置節點的ws狀態更新爲SIGNAL,然後返回false。
到這裏,線程獲取同步鎖的過程便解析完畢,奉上詳細的流程圖:

3.2 釋放鎖過程解析

  釋放鎖的源碼如下:

public final boolean release(int arg) {
        // 調用子類實現的tryRelease來釋放鎖
        if (tryRelease(arg)) {
            Node h = head;
            // 如果釋放成功了,則判斷head節點是否存在,並且不是取消狀態,
            if (h != null && h.waitStatus != 0)
                // 喚醒park中的節點
                unparkSuccessor(h);
            return true;
        }
        return false;
    }
  1. 首先會調用子類實現的tryRelease來釋放鎖;
  2. 如果釋放成功,則判斷head節點是否存在,並且不是取消狀態。這裏的head節點其實就是擁有當前線程的節點。在獲取鎖過程的解析中給出了答案:只有在線程成功獲取鎖的情況下,才能將線程自己所在的節點設置爲head節點。
  3. 喚醒park中的節點,其源碼如下:
private void unparkSuccessor(Node node) {

        // 首先,通過CAS操作將當前ws狀態設置爲0,
        int ws = node.waitStatus;
        if (ws < 0)
            compareAndSetWaitStatus(node, ws, 0);

        // 獲取head節點的後繼節點。
        Node s = node.next;
        // 如果後繼節點不存在或者ws>0(表示後繼節點已經取消了獲取鎖),此時需要將其從隊列中剔除。
        // 具體的剔除方式是從隊列的尾部開始遍歷找到隊列中最前面的節點
        if (s == null || s.waitStatus > 0) {
            s = null;
            for (Node t = tail; t != null && t != node; t = t.prev)
                if (t.waitStatus <= 0)
                    s = t;
        }
        // 如果後繼節點存在,則將其擁有的線程喚醒。
        if (s != null)
            LockSupport.unpark(s.thread);
    }

  釋放鎖的過程比較簡單,但其中也暗含玄機。
  1.爲什麼這裏一定要調用unpark方法?
  答:因爲在獲取鎖的過程中,線程有可能會被park,也有可能不會被park,因此這裏必須要調用unpark方法來確保線程處於可運行的狀態。否則有可能導致隊列永遠的阻塞了。

  2.當後繼節點不存在時,爲什麼要從隊列尾部開始遍歷尋找隊列最前面的節點,而不是從隊列最前面開始遍歷沒有取消的節點?
  答:這個問題先不着急解答,待分析了cancelAcquire()源碼之後,便能瞭然!

3.3 取消鎖獲取源碼解析

  在獲取鎖時,不論何種獲取鎖的方式的任何原因,導致最終獲取鎖失敗了,則會調用cancelAcquire()方法來取消獲取鎖。比如acquireQueued()/acquireInterruptibly()等…
cancelAcquire()方法的源碼如下:

private void cancelAcquire(Node node) {
        // Ignore if node doesn't exist
        if (node == null)
            return;

        node.thread = null;

        // Skip cancelled predecessors
        // 注意,這裏只是維護了每個節點的prev節點,並沒有維護每個節點的next節點
        Node pred = node.prev;
        while (pred.waitStatus > 0)
            node.prev = pred = pred.prev;

        Node predNext = pred.next;
        // 將當前節點的ws狀態設置爲取消狀態。
        node.waitStatus = Node.CANCELLED;
        
        // 從FIFO隊列中剔除當前節點。
        // 如果當前節點是FIFO隊列中的尾節點,則通過CAS操作重新設置tail
        if (node == tail && compareAndSetTail(node, pred)) {
            // 如果設置成功,則通過CAS操作重新設置尾部節點的後繼節點爲null。
            // 這樣,當前節點就從FIFO隊列中剔除了。
            compareAndSetNext(pred, predNext, null);
        } else {
            // 否則,如果當前置節點不是頭節點並且前置節點的ws狀態爲SIGNAL或通過CAS操作設置爲SIGNAL時,說明前置節點已經獲取到同步鎖,此時,會通過CAS操作將當前節點從FIFO隊列中剔除。
            // 如果以上條件不成立,說明當前節點時頭節點,則會調用unparkSuccessor方法喚醒它的後繼節點。
            int ws;
            if (pred != head &&
                ((ws = pred.waitStatus) == Node.SIGNAL ||
                 (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
                pred.thread != null) {
                Node next = node.next;
                if (next != null && next.waitStatus <= 0)
                    compareAndSetNext(pred, predNext, next);
            } else {
                unparkSuccessor(node);
            }

            node.next = node; // help GC
        }
    }

  在3.2中我們遺留了一個問題:後繼節點不存在時,爲什麼要從隊列尾部開始遍歷尋找隊列最前面的節點,而不是從隊列最前面開始遍歷沒有取消的節點?現在,我們可以給出答案了:
在cancelAcquire()方法中,只維護了節點的prev的關係,並沒有維護節點的next關係。因此,只有從FIFO隊列的尾部往前遍歷,才能得到各個節點的正確鏈路關係。

3.4 獲取鎖資源的其他方法及說明

方法名 說明
acquire() 獨佔模式下,獲取鎖資源, 源碼解讀請看3.1章節
acquireShared() 共享模式下,和acquire方法一樣
acquireInterruptibly() 獨佔模式下,和acquire方法一樣,只不過它支持中斷機制,當線程被中斷時,將會拋出InterruptedException異常
acquireSharedInterruptibly() 共享模式下,和acquireInterruptibly方法一樣
tryAcquireNanos() 獨佔模式下,支持在指定nano時間段內獲取自旋,如果在nanos時間內爲獲取到鎖資源,則返回false,表示獲取鎖資源失敗;同樣地,它也支持線程中斷機制。
tryAcquireSharedNanos() 共享模式下,和tryAcquireNanos方法一樣

3.5 釋放鎖資源的其他方法及說明

  獨佔模式下,釋放鎖資源的的方法爲release()方法,它的源碼解讀請看3.3章節。
共享模式下,釋放鎖資源的方法爲releaseShared()方法,本質上它和release()的功能是一樣的,只不過在共享模式下,如果當前線程所在的節點不是頭結點,則會設置它的ws狀態爲PROPAGATE狀態,即無條件的傳播鎖資源。

四. 總結

  同步器是J.U.C包中很多高級併發工具類的基礎框架,比如ReentrantLock、CountDownLatch、Semaphore等,雖然我們在使用這個工具類時只需會應用即可,但是如果某些需求場景需要自定義併發工具類時,對於同步器的原理的深入掌握是必不可少的,另外,對於同步器的掌握一定程度上能夠體現一個開發者對於Java併發的理解程度,因爲它涉及到的理論思想還是挺多的!
  通過本文的介紹,我們知道了同步器的工作原理以及使用方式。實際上同步器的底層是基於很多併發理論、技術和思想的,比如CAS、自旋、volatile、FIFO隊列和對於LockSupport工具類、UnSafe工具類的使用。因此,如果要徹底弄懂同步器的工作原理,很不容易!首先必須要理解Java併發中的這些基本理論和思想。如果對於這些知識還不夠清晰,可以參考前面的Java併發系列文章。

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