併發編程之——AQS原理和阻塞隊列變化

1、AQS簡介

2、源碼分析

2.1 線程阻塞

2.2 線程喚醒


1、AQS簡介

AQS全名:AbstractQueuedSynchronizer,它就是Java的一個抽象類,它的出現是爲了解決多線程競爭共享資源而引發的安全問題,細緻點說AQS具備一套線程阻塞等待以及被喚醒時鎖分配的機制,這個機制AQS是用CLH隊列鎖實現的,即將暫時獲取不到鎖的線程加入到隊列中,隊列是雙向隊列。

常用的實現類是ReentrantLock和CountdownLatch。而且這兩個類都是通過內部類繼承AbstractQueuedSynchronizer,從而實現相應功能的。

 

先看下這張圖,對Java的重入鎖的代碼結構有個大概的瞭解。 

內部抽象類Sync繼承AbstractQueuedSynchronizer,CountdownLatch一樣

 

我們一般使用AQS功能的簡單代碼實現:

public class Demo {
    static Lock lock = new ReentrantLock();

    public static void test() {
        lock.lock();
        try {
            // TODO:
        }
        catch (InterruptedException ex) {
        }
        finally {
            lock.unlock();
        }
    }
}

重入鎖通過加鎖lock 和 解鎖unlock操作進行多線程的同步控制操作。從上面代碼我們可以猜想到,在多線程競爭情況下,當線程加鎖操作獲取不到鎖,則線程要進入阻塞隊列;當鎖釋放後,隊列節點(線程)要能夠獲得鎖;那麼問題來了:

1、線程獲取到鎖具體是怎麼實現的?

2、線程獲取不到鎖具體是怎麼操作的?

3、鎖釋放後,隊列節點是怎麼觸發獲取鎖的?

這些細節問題,一定要看源碼才能得到答案。

 

2、源碼分析

2.1 線程阻塞

以公平鎖爲例:

java.util.concurrent.locks.ReentrantLock$FairSync

    static final class FairSync extends Sync {
        private static final long serialVersionUID = -3000897897090466540L;

        final void lock() {
            acquire(1); // 父類方法,1表示一次
        }

        /**
         * Fair version of tryAcquire.  Don't grant access unless
         * recursive call or no waiters or is first.
         */
        protected final boolean tryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            // 獲取鎖的狀態,其實就是計數器,0表示鎖沒有被任何線程獲得
            int c = getState();
            if (c == 0) {
                // 這裏有三個方法:
                // 1.hasQueuedPredecessors 有沒有前序結點,如果有肯定輪不到當前線程,
                //   這個方法比較巧妙,需要先理解隊列設計思想才能看懂,後面有圖分析
                // 2.compareAndSetState 設置鎖計數器=1,原子操作
                // 3.setExclusiveOwnerThread 設置獨佔
                // 三個操作都符合條件纔算當前線程獲得鎖
                if (!hasQueuedPredecessors() &&
                    compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            else if (current == getExclusiveOwnerThread()) {
                // 如果當前線程已經獲得鎖,那麼鎖計數器state加1
                int nextc = c + acquires;
                if (nextc < 0)
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }
    }

總結:AQS獲取鎖的機制就是維護一個int屬性state,

  • state=0:表示鎖沒人在用
  • state>0:表示有線程獲得鎖,state的值表示線程重入的次數,即多次獲得鎖。

 

java.util.concurrent.locks.AbstractQueuedSynchronizer

    /**
     * The synchronization state.
     */
    private volatile int state;

    public final void acquire(int arg) {
        // 子類(公平/非公平)調用父類的這個方法
        // 如果tryAcquire嘗試加鎖成功就沒有後面方法什麼事了
        // 如果tryAcquire嘗試加鎖是失敗,則先addWaiter把線程加到隊列,然後再acquireQueued嘗試獲取鎖。。。
        if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            // 如果線程被中斷過,再調用一次interrupt方法,清楚中斷狀態
            selfInterrupt();
    }

    /**
     * Creates and enqueues node for current thread and given mode.
     *
     * @param mode Node.EXCLUSIVE for exclusive, Node.SHARED for shared
     * @return the new node
     */
    private Node addWaiter(Node mode) {
        // 先用當前線程構造一個Node結點
        Node node = new Node(Thread.currentThread(), mode);
        // Try the fast path of enq; backup to full enq on failure
        Node pred = tail; // 找尾結點,可能是null,比如:第二個線程過來
        if (pred != null) {
            // 如果排隊的線程很多,給當前線程Node設置好前序結點
            node.prev = pred;
            // compareAndSetTail是將tail指向當前Node,這裏是原子操作
            if (compareAndSetTail(pred, node)) {
                pred.next = node; // 給原尾結點設置後序結點,也就是當前Node
                return node; // 設置完就return
            }
        }

        // 當pred先序結點爲空的時候,有可能需要初始化隊列
        enq(node); 
        return node;
    }

    /**
     * Inserts node into queue, initializing if necessary. See picture above.
     * @param node the node to insert
     * @return node's predecessor
     */
    private Node enq(final Node node) {
        for (;;) { // 死循環操作
            Node t = tail;
            // 可以看到這裏依然對tail做了if-else判斷,爲啥呢?
            // 因爲多線程,這裏有可能tail就是非空,所以上面方法說有可能初始化
            if (t == null) { // Must initialize
                // 初始化分支
                // compareAndSetHead是個原子操作,反正CAS開頭的都是原子操作
                // 需要注意的是,這裏不是用的方法參數node,而是先創建了一個Node,並且head,tail都指向了這個空Node
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {
                // 已經初始化分支
                // 這裏把node設置成tail結點
                node.prev = t;
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t; //返回原來的tail,初始化的時候並不關心返回值,只在xxx的時候關心
                }
            }
        }
    }

    /**
     * Acquires in exclusive uninterruptible mode for thread already in
     * queue. Used by condition wait methods as well as acquire.
     *
     * @param node the node
     * @param arg the acquire argument
     * @return {@code true} if interrupted while waiting
     */
    // 核心方法
    final boolean acquireQueued(final Node node, int arg) {
        // 到這裏,總結一下addWaiter方法
        // addWaiter方法做的事情就是把當前線程封裝成一個node,加入到隊列中,並返回
        boolean failed = true;
        try {
            boolean interrupted = false;
            // 又一個死循環,直到return interrupted獲取鎖
            for (;;) {
                // 這個for循環的意思是:
                // 1、如果當前線程確定還輪不到獲取鎖,則乖乖地進入隊列,並且當前線程中斷
                // 2、如果當前線程可以獲取鎖,則死循環反覆去嘗試獲取鎖,當然,在公平鎖模式下一次就OK了
                final Node p = node.predecessor(); // 取前序結點
                // 如果前序節點是head節點,則嘗試獲取鎖,即第二次嘗試獲取鎖(第一次是tryAcquire)
                if (p == head && tryAcquire(arg)) { // 第二次tryAcquire,也就是在入隊列前再嘗試一把,萬一鎖被釋放了呢!
                    // 如果獲取到鎖,把head指向當前node,把初始化創建的空節點GC
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted; // 這個interrupted表示的是當前線程有沒有被中斷過
                }

                // shouldParkAfterFailedAcquire返回true表示前序節點還在排隊,所以當前節點需要去park,進到parkAndCheckInterrupt方法
                if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed) // 如果上面代碼沒有獲取鎖報錯,需要取消獲取鎖的動作
                cancelAcquire(node); // 取消
        }
    }

    // 獲取鎖失敗後應該排隊
    private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        // 走進這個方法說明前面沒有獲得鎖
        // 先拿到前序節點的狀態
        int ws = pred.waitStatus;
        if (ws == Node.SIGNAL)
            // 前序節點還在排隊呢,所以當前節點node只能掛起,安心地去排隊
            // 這裏說下節點得SIGNAL狀態,它的意思是如果鎖被釋放,應該通知SIGNAL狀態的節點
            /*
             * This node has already set status asking a release
             * to signal it, so it can safely park.
             */
            return true;

        // 下面的情況,node節點不需要去park,最終返回false使上層調用方法死循環直到獲取鎖
        // 首先是ws>0,即waitStatus=cancelled=1(看內部類:Node)
        if (ws > 0) {
            // 如果先序節點的狀態是取消,則把異常的先序節點從隊列中刪除
            /*
             * Predecessor was cancelled. Skip over predecessors and
             * indicate retry.
             */
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
            // 走到這裏,前序節點的waitStatus只能是0/-3,
            /*
             * waitStatus must be 0 or PROPAGATE.  Indicate that we
             * need a signal, but don't park yet.  Caller will need to
             * retry to make sure it cannot acquire before parking.
             */
            // 把前序節點的waitStatus設置爲-1:SIGNAL,因爲啥呢?
            // 看上面if (ws == Node.SIGNAL)分支,只有前序節點waitStatus=-1,當前節點才能安心地去隊列等待,
            // 否則當前node會一直自旋獲取鎖,這顯然是不合理的
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }

    /**
     * Convenience method to park and then check if interrupted
     * 調用底層線程掛起方法將線程掛起,並且返回掛起的狀態,也就是檢查一下
     *
     * @return {@code true} if interrupted
     */
    private final boolean parkAndCheckInterrupt() {
        LockSupport.park(this); // 這裏直接調park方法是將線程掛起
        return Thread.interrupted();
    }

總結:阻塞等待

AQS使用FIFO雙向阻塞隊列來保存被阻塞的線程,實現機制是,AQS通過其內部類Node封裝線程,同時Node維護prev,next,waitStatus信息來實現雙向隊列;

針對節點的waitStatus屬性(等待狀態),要補充說明一下,它的取值有以下幾種:

  • CANCELLED  =  1;  // 取消狀態,唯一個大於0的狀態,表示節點獲取鎖超時。例如:1,2,3三個節點按順序獲取鎖,結果1正在處理業務,2,3排隊等待,2先來的,等久了超時了,而3沒超時,等1釋放鎖以後,3雖然排在2後面,但是會把CANCELLED狀態的2節點刪掉,讓後面未取消的節點頂上來;
  • SIGNAL          = -1;  // 等待觸發狀態,這個狀態的節點就是鎖釋放後需要被通知的節點
  • CONDITION   = -2;  // 等待條件狀態,還沒研究到,先按下不表;
  • PROPAGATE = -3;  // 狀態需要向後傳播
  • 0;未賦值初始化爲0

下面畫圖來說明以下阻塞隊列的初始化和變化:

1、隊列初始化

此時會構造一個空節點放進隊列,鎖的head和tail都指向這個空節點,空節點的thread,prev,next都是null;鎖第一次被獲得就會構造帶有一個空節點的隊列,當然當前線程直接就獲得鎖了,而不會入到隊列。

2、如果有線程競爭,獲取不到鎖的線程就會被封裝成Node節點入到隊列中去,但不是替換空節點,而是跟在空節點的後面;

3、現在鎖釋放了,節點1獲得鎖了,看看隊列的變化,隊列會把head指向節點1,原來的空節點就等着被GC,節點1的thread,prev會被置空,next不變,因爲如果節點1後面還有節點2的話,next就指向節點2;

總之一句話,除了初始化存在空節點以外,隊列的head節點總是最後一個獲得鎖的節點;

2.2 線程喚醒

線程喚醒的前提當然是線程被掛起,線程掛起操作在上面源碼中有貼出來,線程掛起後,線程也就阻塞在這裏:

LockSupport.park(this); // 這裏直接調park方法是將線程掛起

上面acquireQueued方法是AQS的核心,其線程阻塞與獲取鎖都在這個裏面,核心思想是:

1、如果前序節點還在排隊(waitStatus=-1),後續節點直接掛起;

2、如果前序節點取消了(waitStatus=1),後續節點的邏輯中會把取消的前序節點刪除(遞歸刪除);

3、如果前序節點也是剛加進來的,節點狀態還沒定,也沒有獲得鎖,那麼當前線程要把前序節點的waitStatus設置爲-1;

——第3點要好好理解,換句話說,隊列裏除了最後一個節點,其他節點的狀態都是由其next節點來修改的。爲啥要這樣做呢?這是因爲掛起的線程要解除掛起狀態獲取鎖,這需要一個狀態,看下面代碼分析。

 

java.util.concurrent.locks.ReentrantLock$FairSync

    public void unlock() {
        sync.release(1);
    }

java.util.concurrent.locks.AbstractQueuedSynchronizer

    public final boolean release(int arg) {
        // 首先是嘗試釋放鎖,有人問了,這釋放鎖還需要嘗試嗎?又不是獲取鎖,還可能獲取不到
        // 那確實存在釋放不了的情況,什麼情況呢?  那就是重入次數大於1的情況,按照重入鎖的設計,重入幾次就需要釋放幾次
        if (tryRelease(arg)) {
            // 鎖釋放成功,要喚醒後序掛起線程
            Node h = head; // 這裏拿到head節點
            if (h != null && h.waitStatus != 0) // 判斷head節點狀態非0,即被修改過
                // 如果h.waitStatus = 0,表示沒有後續節點,能理解不?看上面第三點
                unparkSuccessor(h);
            return true;
        }
        return false;
    }

    private void unparkSuccessor(Node node) {
    // 注意參數是head節點,因爲喚醒後續節點總是從head往後找
        /*
         * 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)
            // 還原head節點狀態爲0
            compareAndSetWaitStatus(node, ws, 0);

        /*
         * 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;
        // 校驗狀態,因爲隊列裏面可能就沒有其他競爭線程,或者next節點取消了
        if (s == null || s.waitStatus > 0) {
            s = null;
            // 咋整呢?從後往前遍歷,遍歷到最靠前的一個狀態正常的節點,這個節點就是要被喚醒的節點
            // 這裏需要注意的是,這裏並沒有刪除取消的節點,因爲取消是在獲得鎖的邏輯裏面刪除的
            for (Node t = tail; t != null && t != node; t = t.prev)
                if (t.waitStatus <= 0) // 這裏就能理解爲啥都要給前序節點設置狀態等於-1了吧,爲啥不是-2,-3呢?因爲那倆有其他用處
                    s = t;
        }
        if (s != null)
            LockSupport.unpark(s.thread); // 喚醒線程節點
    }

java.util.concurrent.locks.ReentrantLock$Sync

        // 這裏邏輯就很簡單了,判斷state的值即可
        protected final boolean tryRelease(int releases) {
            int c = getState() - releases;
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
            boolean free = false;
            if (c == 0) {
                free = true;
                setExclusiveOwnerThread(null);
            }
            setState(c);
            return free;
        }

最後再來個總結,要說AQS的原理,很多人會談到隊列,計數器,但是更爲底層的支撐,我認爲應該是CAS + LockSupport,如果你詳細看過源碼的話,會發現AQS到處都是CAS操作(CAS操作的本質就是樂觀+自旋),線程的掛起和喚醒則是通過LockSupport來處理。隊列只是其實現的一種方式,換句話說,即便用數組應該也能做的到。

AQS和Synchronized,要說這兩者的區別,有很多,我不想一一列舉了,意義不大,因爲隨着版本的演進,兩者有很多地方都在相互靠攏。

最後談一下LockSupport,其核心功能就park和unpark,掛起和解掛,我看有些文章說重入鎖的掛起不會有線程的用戶/內核態切換,這是錯的,不管是ReentrantLock還是Synchronized,只要發生鎖的競爭,最終都是會有線程的狀態切換的(自旋失敗)。

相對於wait/notify,LockSupport有很多優點,具體這裏不累贅了,只舉個例子吧:先做一次unpark,在做一次park,線程不會掛起;但是先做兩次unpark,再做兩次park,線程就會掛起。

比如說:你去澡堂洗澡,澡堂服務員要給你一個鑰匙牌子掛在手上,才能進澡堂子,但是AQS是一個特殊的澡堂,它只給一個線程服務,而且它只有一個牌子,可以理解爲:這個澡堂子只服務一個顧客,而且只有一個寄存櫃,也就是隻有一個牌子(鑰匙),那麼線程阻塞則好比你去洗澡,但是服務員沒有牌子給你,你只能等,等那個牌子被釋放,你可能問了,不是說只服務我一個顧客嗎? 是的,不錯,但是這個服務員缺心眼啊,它不支持重入啊,它並不會因爲你已經拿到唯一的牌子了,就讓你進去,所以這Java只能自己實現重入鎖了!

再看下上面兩個例子:

1、先做一次unpark,在做一次park

——unpark相當於洗好澡了,把牌子還回去,park相當於去洗澡,發現有牌子,直接拿着牌子去洗澡了,所以線程不會阻塞。

2、先做兩次unpark,再做兩次park

——做兩次unpark,因爲只有一個牌子,所以效果跟做一次unpark一樣,接着,第一次park可以拿到牌子,第二次park就拿不到牌子了,所以線程阻塞。這裏能看出來native代碼傻了吧!

 

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