併發編程之AQS

本篇文章介紹AbstractQueuedSynchronized,內容皆總結摘抄自《Java併發編程的藝術》和《Java併發編程實戰》,僅作筆記。

AQS介紹

隊列同步器AbstractQueuedSynchronizer是用來構建鎖或其他同步組件的基礎框架,使用了一個int成員變量表示同步狀態,通過內置的FIFO隊列來完成資源獲取線程的排隊工作。

同步器的主要使用方式是繼承,子類通過繼承同步器並實現它的抽象方法來管理同步狀態,在同步器中提供了getState()、setState()和compareAndSetState()方法來操作同步狀態,這三個方法可以保證狀態的改變是安全的。同步器並沒有實現任何同步接口,它僅僅是定義了若干同步狀態獲取和釋放的方法來供自定義同步組件使用,同步器既支持獨佔式的獲取同步狀態,也支持共享式獲取同步狀態,這樣方便實現不同類型的同步組件。

同步器是實現鎖的關鍵,在鎖的實現中聚合同步器,利用同步器實現鎖的語義。鎖是面向使用者的,它定義了使用者與鎖交互的接口,隱藏了實現細節;同步器面向的是鎖的實現者,它簡化了鎖的實現方式,屏蔽了同步狀態管理、線程的排隊、等待與喚醒等底層操作。鎖和同步器很好的隔離了使用者和實現者鎖需關注的領域。包括下面要介紹的重入鎖ReentrantLock和讀寫鎖ReentrantReadWriteLock都基於同步器實現。

同步器的設計基於模版方法模式,即使用者需要實現同步器並重寫指定的方法,隨後將同步器組合在自定義同步組件的實現中,並調用同步器提供的模版方法,這些模版方法將會調用使用者重寫的方法。

API

同步器提供了以下三個方法來訪問或修改同步狀態:

//獲取同步狀態值
protected final int getState();
//設置同步狀態的值
protected final void setState(int newState);
//如果當前狀態值等於期望值,則將同步狀態原子的設置爲新值
protected final boolean compareAndSetState(int expect, int update);

除此之外,同步器還有以下常用方法:

//獨佔式獲取同步狀態,如果當前線程獲取同步狀態成功,則由該方法返回,否則,將進入同步隊列等待
//該方法會調用重寫的tryAcquire(int arg)方法
public final void acquire(int arg);

//與acquire(int arg)相同,但該方法響應中斷,當前線程未獲取到同步狀態而進入同步隊列
//如果當前線程被中斷,則該方法會拋出IntertuptedException異常並返回
public final void acquireInterruptibly(int arg);

//共享的獲取同步狀態,如果當前線程未獲取到同步狀態,將會進入同步隊列等待
//與獨佔式獲取的主要區別是同一時刻可以有多個線程獲取到同步狀態
public final void acquireShared(int arg);

//與acquireShared(int arg)相同,該方法響應中斷
public final void acquireSharedInterruptibly(int arg);

//返回一個包含可能正在等待以獨佔模式獲取的線程的集合
public final Collection<Thread> getExclusiveQueuedThreads();

//返回隊列中第一個(等待時間最長的)線程,如果爲null則表示沒有線程在排隊
public final Thread getFirstQueuedThread();

//返回一個包含可能正在等待獲取的線程集合
public final Collection<Thread> getQueuedThreads();

//返回等待獲取的線程數
public final int getQueueLength();

//返回包含正在等待以共享模式獲取的線程的集合
public final Collection<Thread> getSharedQueuedThreads();

//返回是否有其他線程在爭取此同步器
public final boolean hasContended();

//返回是否有線程等待鎖的時間比當前線程長
public final boolean hasQueuedPredecessors();

//是否有線程等待獲取鎖
public final boolean hasQueuedThreads();

//當前同步器是否在獨佔模式下被線程佔用
protected boolean isHeldExclusively();

//指定線程是否在隊列中
public final boolean isQueued(Thread thread);

//獨佔模式釋放同步狀態,該方法會在釋放同步狀態之後,將同步隊列第一個節點包含的線程喚醒
public final boolean release(int arg);

//共享模式釋放指定量的資源
public final boolean releaseShared(int arg);

//嘗試獨佔模式獲取同步狀態,實現該方法需要查詢當前狀態並判斷同步狀態是否符合預期
//然後進行CAS設置同步狀態
protected boolean tryAcquire(int arg)

//在acquireInterruptily(int arg)基礎上增加了超時限制,如果當前線程在超時時間內沒有
//獲取到同步狀態,將會返回false,如果獲取到了則返回true
public final boolean tryAcquireNanos(int arg, long nanosTimeout);

//共享式獲取同步狀態,返回大於等於0的值表示獲取成功,反之,獲取失敗
protected int tryAcquireShared(int arg);

//在acquireSharedInterruptibly(int arg)基礎上增加了超時限制
public final boolean tryAcquireSharedNanos(int arg, long nanosTimeout);

//獨佔式釋放同步狀態,等待獲取同步狀態的線程將有機會獲取同步狀態
protected boolean tryRelease(int arg);

//共享式釋放同步狀態
protected boolean tryReleaseShared(int arg);

同步器提供的模版方法基本上分爲三類:獨佔式獲取與釋放同步狀態、共享式獲取與釋放同步狀態和查詢同步隊列中的等待線程情況。自定義同步組件可以使用同步器提供的模版方法來實現自己的同步語義。

AQS實現分析

同步隊列

同步器依賴內部的同步隊列(一個FIFO雙向隊列)來完成同步狀態的管理,當前線程獲取同步狀態失敗時,同步器會將當前線程以及等待狀態等信息構造爲一個節點(AQS內部類Node)並將其加入同步隊列,同時會阻塞當前線程,當同步狀態釋放時,會把首節點中的線程喚醒,使其再次嘗試獲取同步狀態。

同步隊列中的節點Node用來保存獲取同步狀態失敗的線程引用、等待狀態以及前驅和後繼節點,節點的屬性類型與描述如下表:

屬性類型與名稱 描述
int waitStatus

等待狀態,包含如下狀態:

1. CANCELLED:值爲1,由於在同步隊列中等待的線程等待超時或被中斷,需要從同步隊列中取消等待,節點進入該狀態不會變化。

2. SIGNAL:值爲-1,後繼節點的線程處於等待狀態,當前節點的線程如果釋放了同步狀態或被取消,將會通知後繼節點,使後繼節點的線程得以運行。

3. CONDITION:值爲-2,節點在等待隊列中,節點線程等待在Conditon上,當其他線程對Condition調用了signal()方法後,該節點將會從等待隊列中轉移到同步隊列中,加入到對同步狀態的獲取中。

4. PROPAGATE:值爲-3,表示下一次共享式同步狀態獲取將會無條件的被傳播下去。

5. INITIAL:值爲0,初始狀態。

Node prev 前驅節點,當節點加入同步隊列時被設置
Node next 後繼節點
Node nextWaiter 等待隊列中的後繼結點,如果當前節點是共享的,那麼這個字段將是一個SHARED常量,即節點類型(獨佔和共享)和等待隊列中的後繼節點共用一個字段
Thread thread 獲取同步狀態的線程

節點是構成同步隊列的基礎,同步器擁有首節點head和尾節點tail,沒有成功獲取同步狀態的線程將會成爲節點加入該隊列的尾部。同步隊列的基本結構如下圖:

同步器包含了兩個節點類型的引用,一個指向頭節點,一個指向尾節點。同步隊列遵循FIFO,首節點是獲取同步狀態成功的節點,首節點的線程在釋放同步狀態時,會喚醒後繼節點,而後繼節點在獲取同步狀態成功時將自己設置爲首節點。

獨佔式同步狀態獲取與釋放

通過調用同步器的acquire(int arg)方法可以獲取同步狀態,該方法對中斷不敏感,即由於線程獲取同步狀態失敗後進圖同步隊列,後續對線程進行中斷操作時,線程不會從同步隊列中移出。該方法代碼如下所示:

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

以上代碼首先調用自定義同步器實現的tryAcquire(int arg)方法,該方法保證線程安全的獲取同步狀態,如果同步狀態獲取失敗,則構造同步節點(參數Node.EXCLUSIVE意思是創建的是獨佔式節點)並通過addWaiter(Node node)方法將該節點加入同步隊列的尾部,最後調用acquireQueue(Node node,int arg)方法,使得該節點以“死循環”的方式獲取同步狀態。如果獲取不到則阻塞節點中的線程,而被阻塞線程的喚醒則主要依靠前驅節點的出隊或阻塞線程被中斷來實現。

addWaiter()方法構造節點並加入同步隊列,代碼如下:

private Node addWaiter(Node mode) {
    Node node = new Node(Thread.currentThread(), mode);
    Node pred = tail;
    if (pred != null) {
        node.prev = pred;
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    enq(node);
    return node;
}

private Node enq(final Node node) {
    for (;;) {
        Node t = tail;
        if (t == null) { 
            if (compareAndSetHead(new Node()))
                tail = head;
        } else {
            node.prev = t;
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}

上述代碼中的compareAndSetTail(Node expect,Node update)來確保節點能夠被線程安全的添加,這個方法類似於這篇文章中介紹的CAS,此處不再贅述。

在enq(final Node node)方法中,同步器通過死循環來保證節點的正確添加,只有通過CAS將節點設置爲尾節點後,當前線程才能從該方法返回,否則當前線程便不斷地嘗試。enq()方法將併發添加節點的請求通過CAS變得“串行化”了。

節點進入同步隊列後,就進入自旋的過程,每個節點(線程)都當條件滿足,獲取到了同步狀態,就從這個自旋過程中退出,否則依舊留在這個自旋過程中,並且阻塞節點的線程。自旋過程在acquireQueued()方法中,其代碼如下:

final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {
            final Node p = node.predecessor();
            if (p == head && tryAcquire(arg)) {
                setHead(node);
                p.next = null; 
                failed = false;
                return interrupted;
            }
            if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
                interrupted = true;
            }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

在此方法的循環中,首先判斷當前線程的前驅節點是否是頭節點,如果是頭節點再去嘗試獲取同步狀態,如果不是頭節點則不用嘗試去獲取同步狀態。如果以上兩個條件都滿足則將當前節點設置爲頭結點。

獨佔式同步狀態獲取流程圖如下圖:

上圖中,判斷前驅節點是否爲頭節點以及獲取同步狀態是否成功就是獲取同步狀態的自旋過程。當同步狀態獲取成功後,當前線程從acquire()方法返回,對於鎖這種併發組件而言,代表當前線程獲取了鎖。

當前線程獲取同步狀態並執行相應邏輯後,就需要釋放同步狀態,使得後繼節點能夠繼續獲取同步狀態。通過調用同步器的release(int arg)方法可以釋放同步狀態。該方法在釋放同步狀態之後,會喚醒其後繼節點,進而使後繼節點重新嘗試獲取同步狀態。該方法代碼如下:

public final boolean release(int arg) {
    if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}

在unparkSuccessor()方法中使用LockSupport工具來喚醒出於等待狀態的線程。

在獲取同步狀態時,同步器維護了一個同步隊列,獲取狀態失敗的線程都會創建一個對應的節點對象加入到隊列尾部,並在隊列中自旋。停止自旋的條件是前驅節點是頭節點且當前節點成功獲取到了同步狀態。在釋放同步狀態時,調用tryRelease()方法釋放同步狀態,然後喚醒頭節點的後繼節點。

共享式同步狀態獲取與釋放

共享式獲取與獨佔式獲取最主要的區別在於同一時刻能否有多個線程同時獲取到同步狀態。以文件的讀寫爲例,一個程序在對文件進行寫操作時,這一時刻對於該文件的寫操作都被阻塞,而讀操作能同時進行。寫操作要求對資源的獨佔式訪問,而讀操作可以共享式訪問。

同步器調用acquireShared(int arg)方法可以共享的獲取同步狀態,該方法代碼如下:

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

該方法同樣調用自定義同步器實現的tryAcquireShared(int arg)方法,但tryAcquireShared()方法的返回值爲int類型,當返回值大於等於0時,表示能夠獲取到同步狀態;當返回值小於0時,則調用doAcquireShared()方法。此方法代碼如下:

private void doAcquireShared(int arg) {
    final Node node = addWaiter(Node.SHARED);
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {
            final Node p = node.predecessor();
            if (p == head) {
                int r = tryAcquireShared(arg);
                if (r >= 0) {
                    setHeadAndPropagate(node, r);
                    p.next = null; // help GC
                    if (interrupted)
                        selfInterrupt();
                    failed = false;
                    return;
                }
            }
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

此方法流程與獨佔式獲取類似,首先調用addWaiter()方法創建節點並加入同步隊列尾部,然後判斷其前驅節點是否爲頭節點。如果是頭結點,則嘗試共享式獲取同步狀態,獲取tryAcquireShared()方法返回值。由於共享式獲取同步狀態成功的條件是tryAcquireShared()方法返回值大於等於0,因此當前驅節點是頭節點時判斷tryAcquireShared()返回值如果大於等於0則表示獲取同步狀態成功,退出自旋過程。

共享式獲取同樣需要釋放同步狀態,調用releaseShared()方法可以釋放同步狀態。該方法代碼如下:

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

該方法在喚醒同步狀態後,會喚醒後續處於等待狀態的節點。對於能支持多個線程同時訪問的併發組件,它和獨佔式主要區別在於tryReleaseShared()方法必須確保同步狀態線程安全的釋放,一般是通過循環和CAS來保證的,因爲釋放同步狀態的操作會同時來自多個線程。

獨佔式超時獲取同步狀態

調用同步器的doAcquireNanos(int arg,long nanosTimeout)方法可以超時獲取同步狀態,即在指定的時間段內獲取同步狀態,如果獲取到同步狀態則返回true,否則返回false。

在分析該方法的實現前,先介紹一下響應中斷的同步狀態獲取過程。在Java 5之前,當一個線程獲取不到鎖而被阻塞在synchronized之外時,對該線程進行中斷操作,此時該線程的中斷標誌位會被修改,但線程仍舊會阻塞在synchronized上,等待着獲取鎖。在Java 5後,同步器提供了acquireInterruptibly()方法,這個方法在等待獲取同步狀態時,如果當前線程被中斷,會立刻返回,並拋出InterruptedException。

超時獲取同步狀態的過程可以視作響應中斷獲取同步狀態過程的增強版,doAcquireNanos()方法在支持響應中斷的基礎上,增加了超時獲取的特性。針對超時獲取,主要需要計算出需要睡眠的時間間隔nanosTimeout,爲了防止過早通知,nanosTimeout計算公式爲:nanosTimeout -= now - lastTime,其中now爲當前喚醒時間,lastTime爲上次喚醒時間,如果nanosTimeout大於0則表示超時時間味道,需要繼續睡眠nanosTimeout納秒,反之,表示已經超時。該方法代碼如下:

private boolean doAcquireNanos(int arg, long nanosTimeout)
        throws InterruptedException {
    if (nanosTimeout <= 0L)
        return false;
    final long deadline = System.nanoTime() + nanosTimeout;
    final Node node = addWaiter(Node.EXCLUSIVE);
    boolean failed = true;
    try {
        for (;;) {
            final Node p = node.predecessor();
            if (p == head && tryAcquire(arg)) {
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return true;
            }
            nanosTimeout = deadline - System.nanoTime();
            if (nanosTimeout <= 0L)
                return false;
            if (shouldParkAfterFailedAcquire(p, node) &&
                nanosTimeout > spinForTimeoutThreshold)
                LockSupport.parkNanos(this, nanosTimeout);
            if (Thread.interrupted())
                throw new InterruptedException();
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

 該方法在自旋過程中,判斷前驅節點爲頭節點且獲取同步狀態成功,則從該方法返回,這個過程和獨佔式同步獲取類似,但在同步狀態獲取失敗的處理上有所不同。如果當前線程獲取同步狀態失敗,則判斷是否超時(即判斷nanosTimeout是否小於等於0),如果沒有超時,則重新計算超時間隔nanosTimeout,使當前線程等待nanasTimeout納秒。

如果nanosTimeout小於等於spinForTimeoutThreshod(1000納秒)時,將不會使線程進行超時等待,而是進入快速的自旋過程。因爲非常短的超市等待無法做到十分精確。

獨佔式超時獲取同步態狀態的流程圖如下:

獨佔式超時獲取同步狀態與獨佔式獲取同步狀態在流程上非常相似,主要區別在於未獲取到同步狀態時的處理邏輯。獨佔式獲取在未獲取到同步狀態時,將會使當前線程一直出於等待狀態,而獨佔式超時獲取會使當前線程等待nanosTimeout納秒,如果當前線程在nanosTimeout納秒內沒有獲取到同步狀態,將會從等待邏輯中自動返回。

 

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