Java基礎知識複習(四)

AQS

AQS是一個用來構建鎖和同步器的框架,使用AQS能簡單且高效地構造出應用廣泛的大量的同步器,比如我們提到的 ReentrantLock,Semaphore,其他的注入 ReentrantReadWriteLock,SynchronousQueue,FutureTask等等都是基於AQS的,我們也可以利用AQS構造出符合我們自己需求的同步器。

AQS核心思想

AQS核心思想是,如果被請求的共享資源空閒,則將當前請求資源的線程設置爲有效的工作線程,並且將共享資源設置爲鎖定狀態。如果被請求的共享資源被佔用,那麼就需要一套線程阻塞等待以及被喚醒時鎖分配的機制,這個機制AQS是用CLH隊列鎖實現的,即將暫時獲取不到鎖的線程加入到隊列中。

AQS實現原理

先來看一個AQS原理圖:

在這裏插入圖片描述
AQS維護一個共享資源state,通過內置的FIFO來完成獲取資源線程的排隊工作。(這個內置的同步隊列稱爲"CLH"隊列)。該隊列由一個一個的Node結點組成,每個Node結點維護一個prev引用和next引用,分別指向自己的前驅和後繼結點。AQS維護兩個指針,分別指向隊列頭部head和尾部tail。

當線程獲取資源失敗(比如當前資源已經被其他線程佔用),會被構造成一個Node節點加入CLH隊列中,同時當前線程會被阻塞在隊列中(通過LockSupport.park實現,其實是等待態)。當持有鎖的線程釋放鎖時,會喚醒後繼結點,然後此結點線程繼續加入到對同步狀態的爭奪中。

AQS使用一個int成員變量來表示同步狀態,通過內置的FIFO隊列來完成獲取資源線程的排隊工作。

private volatile int state;

狀態信息通過procted類型的getStatesetStatecompareAndSetState進行操作

AQS支持兩種同步方式:

  • Exclusive

    (獨佔):只有一個線程能執行,如ReentrantLock。又可分爲公平鎖和非公平鎖:

    • 公平鎖:按照線程在隊列中的排隊順序,先到者先拿到鎖
    • 非公平鎖:當線程要獲取鎖時,無視隊列順序直接去搶鎖,誰搶到就是誰的
  • Share(共享):多個線程可同時執行,如Semaphore/CountDownLatch。Semaphore、CountDownLatch、 CyclicBarrier、ReadWriteLock 我們都會在後面講到。

ReentrantReadWriteLock 可以看成是組合式,因爲ReentrantReadWriteLock也就是讀寫鎖允許多個線程同時對某一資源進行讀。

不同的自定義同步器爭用共享資源的方式也不同。自定義同步器在實現時只需要實現共享資源 state 的獲取與釋放方式即可,至於具體線程等待隊列的維護(如獲取資源失敗入隊/喚醒出隊等),AQS已經在頂層實現好了。

AQS底層使用了模板方法模式

同步器的設計是基於模板方法模式的,如果需要自定義同步器一般的方式是這樣(模板方法模式很經典的一個應用):

  1. 使用者繼承AbstractQueuedSynchronizer並重寫指定的方法。(這些重寫方法很簡單,無非是對於共享資源state的獲取和釋放)
  2. 將AQS組合在自定義同步組件的實現中,並調用其模板方法,而這些模板方法會調用使用者重寫的方法。

自定義同步器時需要重寫下面幾個AQS提供的模板方法:

isHeldExclusively()//該線程是否正在獨佔資源。只有用到condition才需要去實現它。
tryAcquire(int)//獨佔方式。嘗試獲取資源,成功則返回true,失敗則返回false。
tryRelease(int)//獨佔方式。嘗試釋放資源,成功則返回true,失敗則返回false。
tryAcquireShared(int)//共享方式。嘗試獲取資源。負數表示失敗;0表示成功,但沒有剩餘可用資源;正數表示成功,且有剩餘資源。
tryReleaseShared(int)//共享方式。嘗試釋放資源,成功則返回true,失敗則返回false。

默認情況下,每個方法都拋出 UnsupportedOperationException。 這些方法的實現必須是內部線程安全的,並且通常應該簡短而不是阻塞。AQS類中的其他方法都是final ,所以無法被其他類使用,只有這幾個方法可以被其他類使用。

AQS源碼分析

Node結點

Node結點是AQS(AbstractQueuedSynchronizer)中的一個靜態內部類,我們撿Node的幾個重要屬性來說一下

static final class Node {
  
    static final Node SHARED = new Node();

    static final Node EXCLUSIVE = null;

 /** waitStatus值,表示線程已被取消(等待超時或者被中斷)*/
    static final int CANCELLED =  1;
  /** waitStatus值,表示後繼線程需要被喚醒(unpaking)*/
    static final int SIGNAL    = -1;
 /**waitStatus值,表示結點線程等待在condition上,當被signal後,會從等待隊列轉移到同步到隊列中 */
    static final int CONDITION = -2;
  /** waitStatus值,表示下一次共享式同步狀態會被無條件地傳播下去*/
    static final int PROPAGATE = -3;

   /** 等待狀態,初始爲0 */
    volatile int waitStatus;

   /**當前結點的前驅結點 */
    volatile Node prev;

    /** 當前結點的後繼結點 */
    volatile Node next;

   /** 與當前結點關聯的排隊中的線程 */
    volatile Thread thread;
獨佔式(默認非公平鎖)

獲取鎖:acquire()

一般調用Lock.lock()方法會直接代理到acquire()

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

大概的邏輯是:

調用使用者重寫的acquire()方法,嘗試獲取鎖,如果成功,返回true,後面的邏輯不再執行;否則,執行下面的方法邏輯。

protected boolean tryAcquire(int arg) {
    throw new UnsupportedOperationException();
}

tryAcquire()的一個實現(默認是非公平鎖,想要使用公平鎖就在創建Lock的時候在構造函數傳入一個true來使用公平鎖):

@ReservedStackAccess
final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState(); // 獲取狀態
    if (c == 0) { // 如果c爲0,說明現在這個資源沒有線程佔用
        if (compareAndSetState(0, acquires)) { // 通過CAS自旋的方式獲取鎖
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    else if (current == getExclusiveOwnerThread()) { // 如果發現自己已經擁有該鎖,簡單的自增
        int nextc = c + acquires;
        if (nextc < 0) // overflow
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    return false; // 否則獲取失敗,返回false
}

如果獲取鎖失敗,會構造一個Node解點,通過addWatiter()將此節點通過CAS自旋的方式,添加到CLH隊列的尾部。

private Node addWaiter(Node mode) {
    Node node = new Node(mode);

    for (;;) {
        Node oldTail = tail;
        if (oldTail != null) {
            node.setPrevRelaxed(oldTail);
            if (compareAndSetTail(oldTail, node)) { // 也是通過CAS自旋的方式進行添加,防止被其他線程佔用
                oldTail.next = node;
                return node;
            }
        } else {
            initializeSyncQueue(); // 如果當前tail爲空或者線程調用CAS設置隊尾失敗,調用這個方法
        }
    }
}
private final void initializeSyncQueue() {
    Node h;
    if (HEAD.compareAndSet(this, null, (h = new Node()))) // 通過CAS自旋的方式來設置結點
        tail = h;
}

構造Node解點後,會放入隊列中:

acquireQueued()內部也是一個死循環,只有前驅結點是頭結點的結點,纔有機會去tryAcquire();若tryAcquire()成功,表示獲取同步狀態成功,將此結點設置爲頭結點;若是非頭結點,或者tryAcquire()失敗,則進入shouldParkAfterFailedAcquire()去判斷判斷當前線程是否應該阻塞,若可以,調用parkAndCheckInterrupt()阻塞當前線程,直到被中斷或者被前驅結點喚醒。若還不能休息,繼續循環。

final boolean acquireQueued(final Node node, int arg) {
    boolean interrupted = false;
    try {
        for (;;) {
            final Node p = node.predecessor();
            if (p == head && tryAcquire(arg)) {
                setHead(node);
                p.next = null; // help GC
                return interrupted;
            }
              // 如果沒有獲取到同步狀態,通過shouldParkAfterFailedAcquire判斷是否應該阻塞,parkAndCheckInterrupt用來阻塞線程
            if (shouldParkAfterFailedAcquire(p, node))
                interrupted |= parkAndCheckInterrupt();
        }
    } catch (Throwable t) {
        cancelAcquire(node);
        if (interrupted)
            selfInterrupt();
        throw t;
    }
}

shouldParkAfterFailedAcquire()

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus;
        if (ws == Node.SIGNAL)
          
            return true;
        if (ws > 0) {
          
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
            pred.compareAndSetWaitStatus(ws, Node.SIGNAL);
        }
        return false;
    }

若shouldParkAfterFailedAcquire返回true,也就是當前結點的前驅結點爲SIGNAL狀態,則意味着當前結點可以放心休息,進入parking狀態了。parkAncCheckInterrupt阻塞線程並處理中斷。

private final boolean parkAndCheckInterrupt() {
        LockSupport.park(this);//使用LockSupport使線程進入阻塞狀態,是本地的native方法
        return Thread.interrupted();// 線程是否被中斷過
    }

簡單總結:

  • 首先tryAcquire獲取同步狀態,成功則直接返回;否則,進入下一環節;
  • 線程獲取同步狀態失敗,就構造一個結點,加入同步隊列中,這個過程使用CAS保證線程安全;
  • 加入隊列中的結點線程進入自旋狀態,若是頭結點,纔有機會嘗試去獲取同步狀態;否則,當其前驅結點的狀態爲SIGNAL,線程便可安心休息,進入阻塞狀態,直到被中斷或者被前驅結點喚醒。
  • 但是呢,由於默認實現的是非公平鎖,即如果鎖現在是釋放狀態,並不是直接由在隊列中的線程排對獲取鎖,也有可能是上一個釋放鎖的線程繼續又獲取到鎖或者是某一個新加入的線程取到鎖,這對處於隊列中的線程不公平(非公平鎖原理)

釋放鎖:release()

當前線程執行完自己的方法邏輯,需要釋放鎖,一般調用Lock.unlock()方法會直接代理到release()

public void unlock() {
    sync.release(1);
}
public final boolean release(int arg) {
    if (tryRelease(arg)) { // 調用使用者重寫的tryRelease方法,若成功,喚醒其後繼結點,失敗則返回false
        Node h = head;
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}

unparkSuccessor()喚醒其後繼結點:

private void unparkSuccessor(Node node) {
    /*
     * If status is negative (i.e., possibly needing signal) try
     * to clear in anticipation of signalling.  It is OK if this
     * fails or if status is changed by waiting thread.
     */
    int ws = node.waitStatus;
    if (ws < 0)
        node.compareAndSetWaitStatus(ws, 0); // 還是採用CAS自旋的方式設置狀態

    /*
     * Thread to unpark is held in successor, which is normally
     * just the next node.  But if cancelled or apparently null,
     * traverse backwards from tail to find the actual
     * non-cancelled successor.
     */
    Node s = node.next;
    if (s == null || s.waitStatus > 0) {
        s = null;
        for (Node p = tail; p != node && p != null; p = p.prev)
            if (p.waitStatus <= 0)
                s = p;
    }
    if (s != null)
        LockSupport.unpark(s.thread);
}

release()的邏輯相對簡單,需要找到頭結點的後繼結點進行喚醒,若後繼結點爲空或處於CANCEL狀態,從後向前遍歷找尋一個正常的結點,喚醒其對應線程。

共享式

共享式:共享式地獲取同步狀態。對於獨佔式同步組件來講,同一時刻只有一個線程能獲取到同步狀態,其他線程都得去排隊等待,其待重寫的嘗試獲取同步狀態的方法tryAcquire返回值爲boolean,這很容易理解;對於共享式同步組件來講,同一時刻可以有多個線程同時獲取到同步狀態,這也是“共享”的意義所在。其待重寫的嘗試獲取同步狀態的方法tryAcquireShared返回值爲int。

 protected int tryAcquireShared(int arg) {
        throw new UnsupportedOperationException();
    }

1.當返回值大於0時,表示獲取同步狀態成功,同時還有剩餘同步狀態可供其他線程獲取;

2.當返回值等於0時,表示獲取同步狀態成功,但沒有可用同步狀態了;

3.當返回值小於0時,表示獲取同步狀態失敗。

獲取鎖:acquireShared():

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

doAcquiredShared():

private void doAcquireShared(int arg) {
    final Node node = addWaiter(Node.SHARED);//構造一個共享結點,添加到同步隊列尾部。若隊列初始爲空,先添加一個無意義的傀儡結點,再將新節點添加到隊列尾部。
    boolean interrupted = false;//線程parking過程中是否被中斷過
    try {
        for (;;) {
            final Node p = node.predecessor(); // 找到前驅結點
            if (p == head) {//頭結點持有同步狀態,只有前驅是頭結點,纔有機會嘗試獲取同步狀態
                int r = tryAcquireShared(arg);//嘗試獲取同步裝填
                if (r >= 0) {//r>=0,獲取成功
                    setHeadAndPropagate(node, r);//獲取成功就將當前結點設置爲頭結點,若還有可用資源,傳播下去,也就是繼續喚醒後繼結點
                    p.next = null; // help GC
                    return;
                }
            }
            if (shouldParkAfterFailedAcquire(p, node))//是否需要進入parking狀態
                interrupted |= parkAndCheckInterrupt();//阻塞線程
        }
    } catch (Throwable t) {
        cancelAcquire(node);
        throw t;
    } finally {
        if (interrupted)
            selfInterrupt();
    }
}

大體邏輯與獨佔式的acquireQueued差距不大,只不過由於是共享式,會有多個線程同時獲取到線程,也可能同時釋放線程,空出很多同步狀態,所以當排隊中的頭結點獲取到同步狀態,如果還有可用資源,會繼續傳播下去。

private void setHeadAndPropagate(Node node, int propagate) {
    Node h = head; // Record old head for check below
    setHead(node);
    
    if (propagate > 0 || h == null || h.waitStatus < 0 ||
        (h = head) == null || h.waitStatus < 0) {
        Node s = node.next;
        if (s == null || s.isShared())
            doReleaseShared();
    }
}

釋放鎖:releaseShared()

public final boolean releaseShared(int arg) {
    if (tryReleaseShared(arg)) { // 嘗試釋放鎖
        doReleaseShared();
        return true;
    }
    return false;
}

doReleaseShared:

private void doReleaseShared() {
    for (;;) {//死循環,共享模式,持有同步狀態的線程可能有多個,採用循環CAS保證線程安全
        Node h = head;
        if (h != null && h != tail) {
            int ws = h.waitStatus;
            if (ws == Node.SIGNAL) {
                if (!h.compareAndSetWaitStatus(Node.SIGNAL, 0))
                    continue;            // loop to recheck cases
                unparkSuccessor(h);
            }
            else if (ws == 0 &&
                     !h.compareAndSetWaitStatus(0, Node.PROPAGATE))
                continue;                // loop on failed CAS
        }
        if (h == head)                   // loop if head changed
            break;
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章