java併發程式設計系列:牛逼的AQS(下)

小孩子4919發表於2019-05-13

標籤: 「我們都是小青蛙」公眾號文章

Condition

ReentrantLock的內部實現

看完了AQS中的底層同步機制,我們來簡單分析一下之前介紹過的ReentrantLock的實現原理。先回顧一下這個顯式鎖的典型使用方式:

Lock lock = new ReentrantLock();
lock.lock();
try {
    加鎖後的程式碼
} finally {
    lock.unlock();     
}
複製程式碼

ReentrantLock首先是一個顯式鎖,它實現了Lock介面。可能你已經忘記了Lock介面長啥樣了,我們再回顧一遍:

public interface Lock {
    void lock();
    void lockInterruptibly() throws InterruptedException;
    boolean tryLock();
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
    void unlock();
    Condition newCondition();
}
複製程式碼

其實ReentrantLock內部定義了一個AQS的子類來輔助它實現鎖的功能,由於ReentrantLock是工作在獨佔模式下的,所以它的lock方法其實是呼叫AQS物件的aquire方法去獲取同步狀態,unlock方法其實是呼叫AQS物件的release方法去釋放同步狀態,這些大家已經很熟了,就不再贅述了,我們大致看一下ReentrantLock的程式碼:

public class ReentrantLock implements Lock {

    private final Sync sync;    //AQS子類物件
    
    abstract static class Sync extends AbstractQueuedSynchronizer { 
        // ... 為節省篇幅,省略其他內容
    }
    
    // ... 為節省篇幅,省略其他內容
}
複製程式碼

所以如果我們簡簡單單寫下下邊這行程式碼:

Lock lock = new ReentrantLock();
複製程式碼

就意味著在記憶體裡建立了一個ReentrantLock物件,一個AQS物件,在AQS物件裡維護著同步佇列head節點和tail節點,不過初始狀態下由於沒有執行緒去競爭鎖,所以同步佇列是空的,畫成圖就是這樣:

image_1c3hf30h3bmodidrvogfe11oh2q.png-16.3kB

Condition的提出

我們前邊嘮叨執行緒間通訊的時候提到過內建鎖的wait/notify機制,等待執行緒的典型的程式碼如下:

synchronized (物件) {
    處理邏輯(可選)
    while(條件不滿足) {
        物件.wait();
    }
    處理邏輯(可選)
}
複製程式碼

通知執行緒的典型的程式碼如下:

synchronized (物件) {
    完成條件
    物件.notifyAll();、
}
複製程式碼

也就是當一個執行緒因為某個條件不能滿足時就可以在持有鎖的情況下呼叫該鎖物件的wait方法,之後該執行緒會釋放鎖並進入到與該鎖物件關聯的等待佇列中等待;如果某個執行緒完成了該等待條件,那麼在持有相同鎖的情況下呼叫該鎖的notify或者notifyAll方法喚醒在與該鎖物件關聯的等待佇列中等待的執行緒。

顯式鎖的本質其實是通過AQS物件獲取和釋放同步狀態,而內建鎖的實現是被封裝在java虛擬機器裡的,我們並沒有講過,這兩者的實現是不一樣的。而wait/notify機制只適用於內建鎖,在顯式鎖裡需要另外定義一套類似的機制,在我們定義這個機制的時候需要整清楚:在獲取鎖的執行緒因為某個條件不滿足時,應該進入哪個等待佇列,在什麼時候釋放鎖,如果某個執行緒完成了該等待條件,那麼在持有相同鎖的情況下怎麼從相應的等待佇列中將等待的執行緒從佇列中移出

為了定義這個等待佇列,設計java的大叔們在AQS中新增了一個名叫ConditionObject的成員內部類:

public abstract class AbstractQueuedSynchronizer {
    
    public class ConditionObject implements Condition, java.io.Serializable {
        private transient Node firstWaiter;
        private transient Node lastWaiter;

        // ... 為省略篇幅,省略其他方法
    }
}
複製程式碼

很顯然,這個ConditionObject維護了一個佇列,firstWaiter是佇列的頭節點引用,lastWaiter是佇列的尾節點引用。但是節點類是Node?對,你沒看錯,就是我們前邊分析的同步佇列裡用到的AQS的靜態內部類Node,怕你忘了,再把這個Node節點類的主要內容寫一遍:

static final class Node {
    volatile int waitStatus;
    volatile Node prev;
    volatile Node next;
    volatile Thread thread;
    Node nextWaiter;
    
    static final int CANCELLED =  1;
    static final int SIGNAL    = -1;
    static final int CONDITION = -2;
    static final int PROPAGATE = -3;
}
複製程式碼

也就是說:AQS中的同步佇列和自定義的等待佇列使用的節點類是同一個

又由於在等待佇列中的執行緒被喚醒的時候需要重新獲取鎖,也就是重新獲取同步狀態,所以該等待佇列必須知道執行緒是在持有哪個鎖的時候開始等待的。設計java的大叔們在Lock介面中提供了這麼一個通過鎖來獲取等待佇列的方法:

Condition newCondition();
複製程式碼

我們上邊介紹的ConditionObject就實現了Condition介面,看一下ReentrantLock鎖是怎麼獲取與它相關的等待佇列的:

public class ReentrantLock implements Lock {

    private final Sync sync;
    
    abstract static class Sync extends AbstractQueuedSynchronizer {
        final ConditionObject newCondition() {
            return new ConditionObject();
        }
        // ... 為節省篇幅,省略其他方法
    }
    
    public Condition newCondition() {
        return sync.newCondition();
    }
    
    // ... 為節省篇幅,省略其他方法
}
複製程式碼

可以看到,其實就是簡單建立了一個ConditionObject物件而已~ 由於 ConditionObject 是AQS 的成員內部類,所以在建立的 ConditionObject 物件中持有 AQS 物件的引用,所以通過 ConditionObject 物件訪問到 同步佇列,也就是可以重新獲取同步狀態,也就是重新獲取鎖 。用文字描述還是有些繞,我們先通過鎖來建立一個Condition物件:

Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
複製程式碼

由於在初始狀態下,沒有執行緒去競爭鎖,所以同步佇列是空的,也沒有執行緒因某個條件不成立而進入等待佇列,所以等待佇列也是空的,ReentrantLock物件、AQS物件以及等待佇列在記憶體中的表示就如圖:

image_1c3hji5a4uvn1sdj1ro33hm87m61.png-26.7kB

當然,這個newCondition方法可以反覆呼叫,從而可以通過一個鎖來生成多個等待佇列

Lock lock = new ReentrantLock();
Condition condition1 = lock.newCondition();
Condition condition2 = lock.newCondition();
複製程式碼

那接下來需要考慮怎麼把執行緒包裝成Node節點放到等待佇列的以及怎麼從等待佇列中移出了。ConditionObject成員內部類實現了一個Condition的介面,這個介面提供了下邊這些方法:

public interface Condition {
    void await() throws InterruptedException;
    long awaitNanos(long nanosTimeout) throws InterruptedException;
    boolean await(long time, TimeUnit unit) throws InterruptedException;
    boolean awaitUntil(Date deadline) throws InterruptedException;
    void awaitUninterruptibly();
    void signal();
    void signalAll();
}
複製程式碼

來看一下這些方法的具體意思:

方法名 描述
void await() 當前執行緒進入等待狀態,直到被通知(呼叫signal或者signalAll方法)或中斷
boolean await(long time, TimeUnit unit) 當前執行緒在指定時間內進入等待狀態,如果超出指定時間或者在等待狀態中被通知或中斷則返回
long awaitNanos(long nanosTimeout) 與上個方法相同,只不過預設使用的時間單位為納秒
boolean awaitUntil(Date deadline) 當前執行緒進入等待狀態,如果到達最後期限或者在等待狀態中被通知或中斷則返回
void awaitUninterruptibly() 當前執行緒進入等待狀態,直到在等待狀態中被通知,需要注意的時,本方法並不相應中斷
void signal() 喚醒一個等待執行緒。
void signalAll() 喚醒所有等待執行緒。

可以看到,Condition中的await方法和內建鎖物件的wait方法的作用是一樣的,都會使當前執行緒進入等待狀態,signal方法和內建鎖物件的notify方法的作用是一樣的,都會喚醒在等待佇列中的執行緒。

像呼叫內建鎖的wait/notify方法時,執行緒需要首先獲取該鎖一樣,呼叫Condition物件的await/siganl方法的執行緒需要首先獲得產生該Condition物件的顯式鎖。它的基本使用方式就是:通過顯式鎖的 newCondition 方法產生Condition物件,執行緒在持有該顯式鎖的情況下可以呼叫生成的Condition物件的 await/signal 方法,一般用法如下:

Lock lock = new ReentrantLock();

Condition condition = lock.newCondition();

//等待執行緒的典型模式
public void conditionAWait() throws InterruptedException {
    lock.lock();    //獲取鎖
    try {
        while (條件不滿足) {
            condition.await();  //使執行緒處於等待狀態
        }
        條件滿足後執行的程式碼;
    } finally {
        lock.unlock();    //釋放鎖
    }
}

//通知執行緒的典型模式
public void conditionSignal() throws InterruptedException {
    lock.lock();    //獲取鎖
    try {
        完成條件;
        condition.signalAll();  //喚醒處於等待狀態的執行緒
    } finally {
        lock.unlock();    //釋放鎖
    }
}
複製程式碼

假設現在有一個鎖和兩個等待佇列:

Lock lock = new ReentrantLock();
Condition condition1 = lock.newCondition();
Condition condition2 = lock.newCondition();
複製程式碼

畫圖表示出來就是:

image_1c3hjlt3n14rl1i9g1epg3371ce16e.png-39.7kB

有3個執行緒maint1t2同時呼叫ReentrantLock物件的lock方法去競爭鎖的話,只有執行緒main獲取到了鎖,所以會把執行緒t1t2包裝成Node節點插入同步佇列,所以ReentrantLock物件、AQS物件和同步佇列的示意圖就是這樣的:

image_1c3hjmnj819nl57f11l11p7f9196r.png-94.5kB

因為此時main執行緒是獲取到鎖處於執行中狀態,但是因為某個條件不滿足,所以它選擇執行下邊的程式碼來進入condition1等待佇列:

lock.lock();
try {
    contition1.await();
} finally {
    lock.unlock();
}
複製程式碼

具體的await程式碼我們就不分析了,太長了,我怕你看的發睏,這裡只看這個await方法做了什麼事情:

  1. condition1等待佇列中建立一個Node節點,這個節點的thread值就是main執行緒,而且waitStatus-2,也就是靜態變數Node.CONDITION,表示表示節點在等待佇列中,由於這個節點是代表執行緒main的,所以就把它叫做main節點把,新建立的節點長這樣:

    image_1c3hs5hvfu3g186m1g8m9tjdg9cg.png-14.1kB

  2. 將該節點插入condition1等待佇列中:

    image_1c3hs74l64m11n6eedc357acvct.png-118.9kB

  3. 因為main執行緒還持有者鎖,所以需要釋放鎖之後通知後邊等待獲取鎖的執行緒t,所以同步佇列裡的0號節點被刪除,執行緒t獲取鎖,節點1稱為head節點,並且把thread欄位設定為null:

    image_1c3hs8phe1r1smrl12q41231hfuda.png-103.4kB

至此,main執行緒的等待操作就做完了,假如現在獲得鎖的t1執行緒也執行下邊的程式碼:

lock.lock();
try {
    contition1.await();
} finally {
    lock.unlock();
}
複製程式碼

還是會執行上邊的過程,把t1執行緒包裝成Node節點插入到condition1等待佇列中去,由於原來在等待佇列中的節點1會被刪除,我們把這個新插入等待佇列代表執行緒t1的節點稱為新節點1吧:

image_1c3hshhsik77531ribb6kv57e4.png-112.2kB

這裡需要特別注意的是:同步佇列是一個雙向連結串列,prev表示前一個節點,next表示後一個節點,而等待佇列是一個單向連結串列,使用nextWaiter表示下一個節點,這是它們不同的地方

現在獲取到鎖的執行緒是t2,大家一起出來混的,前兩個都進去,只剩下t2多不好呀,不過這次不放在condition1佇列後頭了,換成condition2佇列吧:

lock.lock();
try {
    contition2.await();
} finally {
    lock.unlock();
}
複製程式碼

效果就是:

image_1c3hsjumr5jb3c5cqhdk57tieh.png-127.6kB

大家發現,雖然現在沒有執行緒獲取鎖,也沒有執行緒在鎖上等待,但是同步佇列裡仍舊有一個節點,是的,同步佇列只有初始時無任何執行緒因為鎖而阻塞的時候才為空,只要曾經有執行緒因為獲取不到鎖而阻塞,這個佇列就不為空了

至此,maint1t2這三個執行緒都進入到等待狀態了,都進去了誰把它們弄出來呢???額~ 好吧,再弄一個別的執行緒去獲取同一個鎖,比方說執行緒t3去把condition2條件佇列的執行緒去喚醒,可以呼叫這個signal方法:

lock.lock();
try {
    contition2.signal();
} finally {
    lock.unlock();
}
複製程式碼

因為在condition2等待佇列的執行緒只有t2,所以t2會被喚醒,這個過程分兩步進行:

  1. 將在condition2等待佇列的代表執行緒t2新節點2,從等待佇列中移出。

  2. 將移出的節點2放在同步佇列中等待獲取鎖,同時更改該節點的waitStauts0

這個過程的圖示如下:

image_1c3hsv52u8i64rgsdo2t1lngeu.png-119.3kB

如果執行緒t3繼續呼叫signalAllcondition1等待佇列中的執行緒給喚醒也是差不多的意思,只不過會把condition1上的兩個節點同時都移動到同步佇列裡:

lock.lock();
try {
    contition1.signalAll();
} finally {
    lock.unlock();
}
複製程式碼

效果如圖:

image_1c3hthb14i21a58i5168p1a1bfb.png-98.9kB

這樣全部執行緒都從等待狀態中恢復了過來,可以重新競爭鎖進行下一步操作了。

以上就是Condition機制的原理和用法,它其實是內建鎖的wait/notify機制在顯式鎖中的另一種實現,不過原來的一個內建鎖物件只能對應一個等待佇列,現在一個顯式鎖可以產生若干個等待佇列,我們可以根據執行緒的不同等待條件來把執行緒放到不同的等待佇列上去Condition機制的用途可以參考wait/notify機制,我們接下來把之前用內建鎖和wait/notify機制編寫的同步佇列BlockedQueue顯式鎖 + Condition的方式來該寫一下:

import java.util.LinkedList;
import java.util.Queue;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class ConditionBlockedQueue<E> {

    private Lock lock = new ReentrantLock();

    private Condition notEmptyCondition = lock.newCondition();

    private Condition notFullCondition = lock.newCondition();

    private Queue<E> queue = new LinkedList<>();

    private int limit;

    public ConditionBlockedQueue(int limit) {
        this.limit = limit;
    }

    public int size() {
        lock.lock();
        try {
            return queue.size();
        } finally {
            lock.unlock();
        }
    }

    public boolean add(E e) throws InterruptedException {
        lock.lock();
        try {
            while (size() >= limit) {
                notFullCondition.await();
            }

            boolean result = queue.add(e);
            notEmptyCondition.signal();
            return result;
        } finally {
            lock.unlock();
        }
    }

    public E remove() throws InterruptedException{
        lock.lock();
        try {
            while (size() == 0) {
                notEmptyCondition.await();
            }
            E e = queue.remove();
            notFullCondition.signalAll();
            return e;
        } finally {
            lock.unlock();
        }
    }
}
複製程式碼

在這個佇列裡邊我們用了一個ReentrantLock鎖,通過這個鎖生成了兩個Condition物件,notFullCondition表示佇列未滿的條件,notEmptyCondition表示佇列未空的條件。當佇列已滿的時候,執行緒會在notFullCondition上等待,每插入一個元素,會通知在notEmptyCondition條件上等待的執行緒;當佇列已空的時候,執行緒會在notEmptyCondition上等待,每移除一個元素,會通知在notFullCondition條件上等待的執行緒。這樣語義就變得很明顯了。如果你有更多的等待條件,你可以通過顯式鎖生成更多的Condition物件。而每個內建鎖物件都只能有一個相關聯的等待佇列,這也是顯式鎖對內建鎖的優勢之一

我們總結一下上邊的用法:每個顯式鎖物件又可以產生若干個Condition物件,每個Condition物件都會對應一個等待佇列,所以就起到了一個顯式鎖對應多個等待佇列的效果

AQS中其他針對等待佇列的重要方法

除了Condition物件的awaitsignal方法,AQS還提供了許多直接訪問這個佇列的方法,它們由都是public final修飾的:

public abstract class AbstractQueuedSynchronizer {
    public final boolean owns(ConditionObject condition)
     public final boolean hasWaiters(ConditionObject condition) {}
     public final int getWaitQueueLength(ConditionObject condition) {}
     public final Collection<Thread> getWaitingThreads(ConditionObject condition) {}
}
複製程式碼
方法名 描述
owns 查詢是否通過本AQS物件生成的指定的 ConditionObject物件
hasWaiters 指定的等待佇列裡是否有等待執行緒
getWaitQueueLength 返回正在等待此條件的執行緒數估計值。因為在構造該結果時,多執行緒環境下實際執行緒集合可能發生大的變化
getWaitingThreads 返回正在等待此條件的執行緒集合的估計值。因為在構造該結果時,多執行緒環境下實際執行緒集合可能發生大的變化

如果有需要的話,可以在我們自定義的同步工具中使用它們。

題外話

寫文章挺累的,有時候你覺得閱讀挺流暢的,那其實是背後無數次修改的結果。如果你覺得不錯請幫忙轉發一下,萬分感謝~ 這裡是我的公眾號「我們都是小青蛙」,裡邊有更多技術乾貨,時不時扯一下犢子,歡迎關注:

java併發程式設計系列:牛逼的AQS(下)

小冊

另外,作者還寫了一本MySQL小冊:《MySQL是怎樣執行的:從根兒上理解MySQL》的連結 。小冊的內容主要是從小白的角度出發,用比較通俗的語言講解關於MySQL進階的一些核心概念,比如記錄、索引、頁面、表空間、查詢優化、事務和鎖等,總共的字數大約是三四十萬字,配有上百幅原創插圖。主要是想降低普通程式設計師學習MySQL進階的難度,讓學習曲線更平滑一點~ 有在MySQL進階方面有疑惑的同學可以看一下:

java併發程式設計系列:牛逼的AQS(下)

相關文章