AQS:JAVA經典之鎖實現演算法(二)-Condition

秋褲Boy發表於2019-02-28

零:序言

使用過ReentrantLock的盆友應該也知道Condition的存在。先講解下它存在的意義:就是仿照實現Object類的wait signal signallAll等函式功能的。

這裡引申一個面試常問到的問題:wait會釋放鎖,sleep不會。

  • Condition的通常使用場景是這樣的: 生產者消費者模型,假設生產者只有在生產佇列為空時才進行生產,則程式碼類似如下:
Condition emptyCondition = ReentrantLock.newCondition();
Runnable consumer = new Runnable() {
  public void run() {
    if(queue.isEmpty()) {
      emptyCondition.signal();  // emptyObj.notify();
    } else {
      consumer.consume();
    }
  }
}
Runnable provider = new Runnable() {
  public void run() {
    emptyCondition.wait();  // emptyObj.wait();
    providerInstance.produce();
  }
}
複製程式碼

所以我們可以知道Condition設計的意義了。下面我們來講解下其實現原理。

一:實現概況

還記得在AQS:JAVA經典之鎖實現演算法(一)提到的鎖實現的Sync Queue嗎? Condition的實現是類似的原理: 每個AQS裡有x(視你newCondition幾次)個Condition Queue,它的結點類也是AQS內部類NodeNode裡有一個nextWaiter,指向下一個在同一Condition Queue裡的Node。 結構如下圖:

Condition Queue.png

  • 首先明確下是,condition.wait一定是在成功lock的執行緒裡呼叫才有效,不然不符合邏輯,同時也會丟擲IlleagleMornitorException
  • 獲取鎖的執行緒處於Sync Queue的隊首,當呼叫condition.wait時,該執行緒會釋放鎖(即將AQSstate置為0),同時喚醒後繼結點,後繼結點在acquire的迴圈裡會成功獲取鎖,然後將自己所在結點置為隊首,然後開始自己執行緒自己的業務程式碼。 這個過程看下圖:
    wait狀態圖_1

wait狀態圖_2

  • 當waiter_1收到相應conditionsignal後,在Condition Queue中的Node會從Condition Queue中出隊,進入Sync Queue佇列,開始它的鎖競爭的過程。 過程看下圖:

signal狀態圖_1

signal狀態圖_2

所以,這裡可以看出來,即使是被signal了,被signal的執行緒也不是直接就開始跑,而是再次進入Sync Queue開始競爭鎖而已。這裡的這個邏輯,跟Object.wait Object.signal也是完全一樣的。

二:程式碼實現原理

我們先看一段運用到condition的程式碼案例: 假設生成者在生產佇列queue為空時emptyCondition.signal才進行生產操作

ReentrantLock locker = new ReentrantLock();
Condition emptyCondition = locker.newCondition();

Runnable consumer = new Runnable() {
  public void run() {
    locker.lock();
    if (queue.isEmpty()) {
      emptyCondition.signal();
    } else {
      ...
    }
    locker.unlock();
  }
};

Runnable producer = new Runnable() {
  public void run() {
    locker.lock();
    emptyCondition.wait();
    // 開始生產
    ...
    locker.unlock();
  }
}
複製程式碼

我們從消費者一步一步走,擬定如下這樣一套執行緒切換邏輯:

  • producer#lock
  • consumer#lock
  • producer#await
  • consumer#signal
  • consumer#unlock
  • producer#unlock

(先從Sync Queue Condition Queue圖解講一遍,然後對應圖解,對著程式碼擼一遍)


  • producer#lock

生產者直接獲取鎖成功,入隊Sync Queue,位隊首

producer#lock後queue狀態

consumer#lock

消費者競爭鎖失敗,進入Sync Queue等待獲取鎖

consumer#lock後queue狀態

  • producer#await

生產者進入等待,釋放鎖,出Sync Queue,進入Condition Queue,等待emptyCondition來喚醒。

producer#wait後Queue狀態

  • consumer#signal

消費者喚起生產者,生產者consumernodeCondition Queue轉移到Sync Queue開始競爭鎖。

consumer#signal後Queue狀態

  • consumer.unlock

consumer釋放鎖後,consumernodeSync Queue出隊,釋放state,喚醒後繼結點provider#nodeprovider搶佔到鎖。

consumer#unlock後Queue狀態

  • provider#unlock

這裡就沒有啥好說的了。

當然,我為了講解過程,像在鎖被第一次成功獲取的時候,邏輯上雖然並不是直接進入Sync Queue我也給講解成直接進入Sync Queue了,這是為了縮減邊邊角角的小邏輯,講清楚主線邏輯。大家看明白主邏輯,然後再自己去擼一遍,就融會貫通了。

三:程式碼擼一把

  • provider.lock

        final void lock() {
            // 這就直接獲取鎖成功了,沒有else的邏輯了
            if (compareAndSetState(0, 1))
                // 這個方法是AQS類用來設定擁有鎖的執行緒例項
                setExclusiveOwnerThread(Thread.currentThread());
            else
                acquire(1);
        }
複製程式碼
  • consumer#lock

        final void lock() {
            if (compareAndSetState(0, 1))
                setExclusiveOwnerThread(Thread.currentThread());
            // consumer.lock就要走這裡了,因為上面的compareAndSetState
            // 返回false
            else
                acquire(1);
        }
複製程式碼
    protected final boolean compareAndSetState(int expect, int update) {
        // 樓下這個是CAS原理進行值修改,CAS就對比樂觀鎖來,
        // 這裡想要修改this這個物件的state欄位,如果state是expect
        // 則修改至update,返回true;否則false。我們知道provider.lock
        // 已經將state 改為非0值了,所以這裡肯定失敗啦
        return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
    }
複製程式碼
  • provider#await

先簡單看下Condition類物件結構

    public class ConditionObject implements Condition, java.io.Serializable {
        private static final long serialVersionUID = 1173984872572414699L;
        /** First node of condition queue. */
        private transient Node firstWaiter;
        /** Last node of condition queue. */
        private transient Node lastWaiter;
}
...
複製程式碼

一個Condition物件就是一條鏈隊,頭尾結點在Condition的內部欄位指定firstWaiter lastWaiter

await方法

        public final void await() throws InterruptedException {
            // 因為await是響應中斷的等待,這裡就是檢驗下,
            // 通常而言,凡是throws InterruptedException的,
            // 開頭基本都是這句
            if (Thread.interrupted())
                throw new InterruptedException();
            // 這裡是向condition queue中插入一個node,並返回之,
            // 插入了這個node,就代表當前執行緒在condition queue
            // 中開始等待了
            Node node = addConditionWaiter();
            // 這個是AQS釋放鎖方法,加個fully,就是用來將多次
            // 獲取鎖一次性都釋放掉,然後將鎖獲取次數返回,
            // 留著後面signal後成功獲取鎖的時候,還要加鎖同樣的
            // 次數。
            // !!!同時注意,這裡喚醒了後繼結點!後集結點就繼續開始
            // 競爭鎖,就是在acquire那個自旋方法裡,記得嗎
            // 不記得去看看文章(一)
            int savedState = fullyRelease(node);
            // 記錄當前執行緒中斷的標記
            int interruptMode = 0;
            // 判斷當前的node是否已經轉移到sync queue裡了。
            // 轉移了,說明這個node已經開始競爭鎖了,不用再等待
            // 喚醒了,沒轉,繼續自旋
            while (!isOnSyncQueue(node)) {
                // 這裡把當前執行緒給掛起了
                LockSupport.park(this);
                // 這裡的方法checkxxx就是用來檢查waiting自旋期間,執行緒有沒有
                // interrupt掉。因為await方法是響應執行緒中斷的。
                // 若interrupt了,則在checkxxx方法裡,會將node轉移到
                // sync Queue中,去競爭,不要擔心,因為同時
                // 會設定interruptMode,在最後會根據其值拋Interrupted
                // 異常。。
                if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
                    break;
                // 那什麼時候就結束上面的自旋呢?一個是當前的執行緒被
                // signal了,那node就被transfer到sync queue了,while
                // 就不滿足了。再一個就是執行緒中斷了,在while迴圈體裡給break掉了
            }
            // 跳出來後,緊接著去競爭鎖,知道成功為止。&& 後面這個THROW_IE,標識
            // 要丟擲異常,不是的話,就是REINTERRPUT,代表保證執行緒的中斷標記不被
            // 重置即可。
            if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
                interruptMode = REINTERRUPT;
            // 這兒是在condition queue裡有多個waiter的時候才起作用,主要用來將
            // CANCEL的結點從鏈隊中剔除掉
            // 具體大家自己看吧。現在忽略這
            if (node.nextWaiter != null) // clean up if cancelled
                unlinkCancelledWaiters();
            // 這兒就是處理interruptMode中斷標記欄位的邏輯
            // 在reportxxx中,interruptMode為THROW_IE,則丟擲
            // 異常,不是,則保證執行緒的中斷field不被重置為“未中斷”即可
            if (interruptMode != 0)
                reportInterruptAfterWait(interruptMode);
        }
複製程式碼
  • consumer#signal

consumer在呼叫emptyCondition.signal的時候,會影響到emptyConditioncondition queue中的等待執行緒,這裡 具體指上面的provider#await方法。

        public final void signal() {
            // 先判斷下,lock鎖是不是在呼叫signal方法的當前執行緒手裡
            if (!isHeldExclusively())
                throw new IllegalMonitorStateException();
            // 取到condition queue裡的第一個waiter node,這裡也就是
            // consumer,因為它第一個await進入condition queue了
            Node first = firstWaiter;
            // 這裡去進行了具體的signal操作,具體會做先把waiter node的waitStatus
            // 從CONDITION狀態改為入Sync Queue的正常狀態值0
            // 然後修改Sync Queue 的Head Tail等,讓其入隊成功
            // 最後再從其前驅結點的狀態值上確保當前結點能夠被喚起即可。
            // 這裡是因為這個waitStatus值對後繼結點的行為是有影響的,像SIGNAL指
            // 的是在結點釋放後,要去喚醒後繼結點
            // 
            if (first != null)
                doSignal(first);
        }
複製程式碼
  • consumer#unlock

unlock具體呼叫的 AQSrelease()方法

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

    // AQS.release
    public final boolean release(int arg) {
        // tryRelease,這裡由NonFairSync實現,具體就是通過
        // CAS去修改state值,並判斷是否成功釋放鎖
        if (tryRelease(arg)) {
            // 成功釋放了,則在waitStatus 不是初始狀態時,去喚醒後繼,
            // 這個 != 0 來做判斷的原因,就要綜合所有情況,
            // 像FailSync NonFairSync \ Exclusive \ Share
            // 等所有情況來看這裡的waitSTatus都會處於什麼狀態。
            // 全擼一遍的話,會發現這裡的 != 0能夠涵蓋以上所有情況。
            Node h = head;
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
    }
複製程式碼
  • provider#unlock

這裡就同理上面了。

總結

總體來看兩個 queue的轉換還是挺清楚的。只要記住,不管什麼情況(中斷與否),都是要從condition queue轉移到sync queue的。具體大家還是要自己去想一種執行緒切換場景,去走走看。 行文匆匆, 歡迎指正。

相關文章