1. 程式人生 > >【搞定Java併發程式設計】第19篇:重入鎖 --- ReentrantLock 詳解

【搞定Java併發程式設計】第19篇:重入鎖 --- ReentrantLock 詳解

  • AQS系列文章:

1、佇列同步器AQS原始碼分析之概要分析

2、佇列同步器AQS原始碼分析之獨佔模式

3、佇列同步器AQS原始碼分析之共享模式

4、佇列同步器AQS原始碼分析之Condition介面、等待佇列

  • 先推薦兩篇好文章:

1、深入剖析基於併發AQS的(獨佔鎖)重入鎖(ReetrantLock)及其Condition實現原理【寫的非常好】

2、 ReentrantLock原始碼分析

說明:本文內容大部分均出自與上面這兩篇文章之中。


本文目錄:

1、重入鎖ReentrantLock的基本概念

2、重入鎖ReentrantLock與synchronized關鍵字的對比

3、重入鎖的一個簡單案例

4、重入鎖ReentrantLock的原始碼分析

4.1、獲取鎖和釋放鎖

4.2、公平鎖和非公平鎖

5、等待佇列的實現機制


1、重入鎖ReentrantLock的基本概念

重入鎖ReentrantLock,顧名思義,就是支援重進入的鎖,它表示該執行緒能夠支援一個執行緒對資源的重複加鎖。除此之外,重入鎖還支援獲取鎖時的公平和非公平性選擇。

所謂的公平與非公平指的是在請求先後順序上,先對鎖進行請求的就一定先獲取到鎖,那麼這就是公平鎖,反之,如果對於鎖的獲取並沒有時間上的先後順序,如後請求的執行緒可能先獲取到鎖,這就是非公平鎖,一般而言非,非公平鎖機制的效率往往會勝過公平鎖的機制,但在某些場景下,可能更注重時間先後順序,那麼公平鎖自然是很好的選擇。需要注意的是ReetrantLock支援對同一執行緒重加鎖,但是加鎖多少次,就必須解鎖多少次,這樣才可以成功釋放鎖。

ReetrantLock是基於AQS併發框架實現的。這裡簡單回顧下AQS的工作原理:

AbstractQueuedSynchronizer又稱為佇列同步器(後面簡稱AQS),它是用來構建鎖或其他同步元件的基礎框架,內部通過一個 int 型別的成員變數 state 來控制同步狀態,當 state=0 時,則說明沒有任何執行緒佔有共享資源的鎖,當 state=1 時,則說明有執行緒目前正在使用共享變數,其他執行緒必須加入同步佇列進行等待。AQS內部通過內部類 Node 構成 FIFO 的同步佇列來完成執行緒獲取鎖的排隊工作,同時利用內部類 ConditionObject 構建等待佇列,當 Condition 呼叫 wait() 方法後,執行緒將會加入等待佇列中,而當 Condition 呼叫 signal() 方法後,執行緒將從等待佇列轉移動同步佇列中進行鎖競爭。注意這裡涉及到兩種佇列,一種的同步佇列,當執行緒請求鎖而等待的後將加入同步佇列等待,而另一種則是等待佇列(可有多個),通過Condition呼叫await()方法釋放鎖後,將加入等待佇列。

AQS作為基礎元件,對於鎖的實現存在兩種不同的模式,即共享模式(如Semaphore)和獨佔模式(如ReetrantLock),無論是共享模式還是獨佔模式的實現類,其內部都是基於AQS實現的,也都維持著一個虛擬的同步佇列,當請求鎖的執行緒超過現有模式的限制時,會將執行緒包裝成Node結點並將執行緒當前必要的資訊儲存到node結點中,然後加入同步佇列等待獲取鎖,而這一系列操作都是AQS協助我們完成的。這也是AQS作為基礎元件的原因,無論是Semaphore還是ReetrantLock,其內部絕大多數方法都是間接呼叫AQS完成的。

下面就看下ReentrantLock 和 AQS 之間的關係:

圖片來自: https://blog.csdn.net/javazejian/article/details/75043422
  • AbstractOwnableSynchronizer:抽象類,定義了儲存獨佔當前鎖的執行緒和獲取的方法。
  • AbstractQueuedSynchronizer:抽象類,AQS框架核心類,其內部以虛擬佇列的方式管理執行緒的鎖獲取與鎖釋放,其中獲取鎖(tryAcquire方法)和釋放鎖(tryRelease方法)並沒有提供預設實現,需要子類重寫這兩個方法實現具體邏輯,目的是使開發人員可以自由定義獲取鎖以及釋放鎖的方式。
  • Node:AbstractQueuedSynchronizer 的內部類,用於構建虛擬佇列(連結串列雙向連結串列),管理需要獲取鎖的執行緒。
  • Sync:抽象類,是ReentrantLock的內部類,繼承自AbstractQueuedSynchronizer,實現了釋放鎖的操作(tryRelease()方法),並提供了lock抽象方法,由其子類實現。
  • NonfairSync:是ReentrantLock的內部類,繼承自Sync,非公平鎖的實現類。
  • FairSync:是ReentrantLock的內部類,繼承自Sync,公平鎖的實現類。
  • ReentrantLock:實現了Lock介面的,其內部類有Sync、NonfairSync、FairSync,在建立時可以根據fair引數決定建立NonfairSync(預設非公平鎖)還是FairSync。

ReentrantLock內部存在3個實現類,分別是Sync、NonfairSync、FairSync。其中Sync繼承自AQS實現瞭解鎖tryRelease()方法,而NonfairSync(非公平鎖)、 FairSync(公平鎖)則繼承自Sync,實現了獲取鎖的tryAcquire()方法。ReentrantLock的所有方法呼叫都通過間接呼叫AQS和Sync類及其子類來完成的。

從上述類圖可以看出AQS是一個抽象類,但請注意其原始碼中並沒一個抽象的方法,這是因為AQS只是作為一個基礎元件,並不希望直接作為直接操作類對外輸出,而更傾向於作為基礎元件,為真正的實現類提供基礎設施。如構建同步佇列,控制同步狀態等,事實上,從設計模式角度來看,AQS採用的模板模式的方式構建的,其內部除了提供併發操作核心方法以及同步佇列操作外,還提供了一些模板方法讓子類自己實現,如加鎖操作以及解鎖操作,

為什麼這麼做?這是因為AQS作為基礎元件,封裝的是核心併發操作,但是實現上分為兩種模式,即共享模式與獨佔模式,而這兩種模式的加鎖與解鎖實現方式是不一樣的,但AQS只關注內部公共方法實現並不關心外部不同模式的實現,所以提供了模板方法給子類使用,也就是說實現獨佔鎖,如ReentrantLock需要自己實現tryAcquire()方法和tryRelease()方法,而實現共享模式的Semaphore,則需要實現tryAcquireShared()方法和tryReleaseShared()方法,這樣做的好處是顯而易見的,無論是共享模式還是獨佔模式,其基礎的實現都是同一套元件(AQS),只不過是加鎖解鎖的邏輯不同罷了,更重要的是如果我們需要自定義鎖的話,也變得非常簡單,只需要選擇不同的模式實現不同的加鎖和解鎖的模板方法即可。


2、重入鎖ReentrantLock與synchronized關鍵字的對比

2.1、synchronized關鍵字回顧

先回顧下synchronized的相關知識:【具體可點選:synchronized關鍵字詳解

Java提供了內建鎖來支援多執行緒的同步,JVM根據synchronized關鍵字來標識同步程式碼塊,當執行緒進入同步程式碼塊時會自動獲取鎖,退出同步程式碼塊時會自動釋放鎖,一個執行緒獲得鎖後其他執行緒將會被阻塞。

每個Java物件都可以用做一個實現同步的鎖,synchronized關鍵字可以用來修飾物件方法,靜態方法和程式碼塊,當修飾物件方法和靜態方法時鎖分別是方法所在的物件和Class物件,當修飾程式碼塊時需提供額外的物件作為鎖。每個Java物件之所以可以作為鎖,是因為在物件頭中關聯了一個monitor物件(管程),執行緒進入同步程式碼塊時會自動持有monitor物件,退出時會自動釋放monitor物件,當monitor物件被持有時其他執行緒將會被阻塞。當然這些同步操作都由JVM底層幫你實現了,但以synchronized關鍵字修飾的方法和程式碼塊在底層實現上還是有些區別的。

synchronized關鍵字修飾的方法是隱式同步的,即無需通過位元組碼指令來控制的,JVM可以根據方法表中的ACC_SYNCHRONIZED訪問標誌來區分一個方法是否是同步方法;

而synchronized關鍵字修飾的程式碼塊是顯式同步的,它是通過monitorentermonitorexit位元組碼指令來控制執行緒對管程的持有和釋放。

monitor物件內部持有_count欄位,_count等於0表示管程未被持有,_count大於0表示管程已被持有,每次持有執行緒重入時_count都會加1,每次持有執行緒退出時_count都會減1,這就是內建鎖重入性的實現原理。另外,monitor物件內部還有兩條佇列_EntryList和_WaitSet,對應著AQS的同步佇列和條件佇列,當執行緒獲取鎖失敗時會到_EntryList中阻塞,當呼叫鎖物件的wait方法時執行緒將會進入_WaitSet中等待,這是內建鎖的執行緒同步和條件等待的實現原理。

2.2、ReentrantLock和synchronized的比較

在Java5.0之前,協調對共享物件的訪問可以使用的機制只有synchronized和volatile。我們知道synchronized關鍵字實現了內建鎖,而volatile關鍵字保證了多執行緒的記憶體可見性。在大多數情況下,這些機制都能很好地完成工作,但卻無法實現一些更高階的功能,例如,無法中斷一個正在等待獲取鎖的執行緒,無法實現限定時間的獲取鎖機制,無法實現非阻塞結構的加鎖規則等。而這些更靈活的加鎖機制通常都能夠提供更好的活躍性或效能。因此,在Java5.0中增加了一種新的機制:ReentrantLock。

ReentrantLock類實現了Lock介面,並提供了與synchronized相同的互斥性和記憶體可見性,它的底層是通過AQS來實現多執行緒同步的。與內建鎖相比ReentrantLock不僅提供了更豐富的加鎖機制,而且在效能上也不遜色於內建鎖(在以前的版本中甚至優於內建鎖)。

ReentrantLock和synchronized的主要區別如下:

1、synchronized關鍵字是Java提供的內建鎖機制,其同步操作由底層JVM實現,而ReentrantLock是java.util.concurrent包提供的顯式鎖,其同步操作由AQS同步器提供支援。

2、ReentrantLock在加鎖和記憶體上提供的語義與內建鎖相同,此外它還提供了一些其他功能,包括定時的鎖等待,可中斷的鎖等待,公平鎖,以及實現非塊結構的加鎖。

3、事實上確實有許多人使用ReentrantLock來替代synchronized關鍵字的加鎖操作。但是內建鎖仍然有它特有的優勢,內建鎖為許多開發人員所熟悉,使用方式也更加的簡潔緊湊,因為顯式鎖必須手動在finally塊中呼叫unlock,所以使用內建鎖相對來說會更加安全些。

4、同時未來更加可能會去提升synchronized而不是ReentrantLock的效能。因為synchronized是JVM的內建屬性,它能執行一些優化,例如對執行緒封閉的鎖物件的鎖消除優化,通過增加鎖的粒度來消除內建鎖的同步,而如果通過基於類庫的鎖來實現這些功能,則可能性不大。

在JDK 1.6之後,虛擬機器對於synchronized關鍵字進行整體優化後,在效能上synchronized與ReentrantLock已沒有明顯差距,因此在使用選擇上,需要根據場景而定,大部分情況下我們依然建議是synchronized關鍵字,原因之一是使用方便語義清晰,二是效能上虛擬機器已為我們自動優化。而ReentrantLock提供了多樣化的同步特性,如超時獲取鎖、可以被中斷獲取鎖(synchronized的同步是不能中斷的)、等待喚醒機制的多個條件變數(Condition)等,因此當我們確實需要使用到這些功能是,可以選擇ReentrantLock。


3、重入鎖的一個簡單案例

package zju.com.lock;

import java.util.concurrent.locks.ReentrantLock;

public class ReentrantLockDemo implements Runnable {

	public static ReentrantLock lock = new ReentrantLock();
	public static int count = 0;
	
	@Override
	public void run() {
		for(int j = 0; j < 10000000; j++){
			lock.lock();
			// 支援重入鎖
			lock.lock();
			try {
				count++;   // 臨界區:共享變數
			} finally {
				// 執行兩次解鎖
				lock.unlock();
				lock.unlock();				
			}
		}
	}

	public static void main(String[] args) throws InterruptedException {
		
		ReentrantLockDemo tld = new ReentrantLockDemo();
		Thread t1 = new Thread(tld);
		Thread t2 = new Thread(tld);
		
		t1.start();
		t2.start();
		
		t1.join();
		t2.join();
		
		// 產看count的結果
		System.out.println(count);  // 20000000
	}
}

案例程式碼非常簡單,我們使用兩個執行緒同時操作臨界資源 i,執行自增操作,使用ReenterLock進行加鎖,解決執行緒安全問題,這裡進行了兩次重複加鎖。由於ReenterLock支援重入,因此這樣是沒有問題的,需要注意的是在finally程式碼塊中,需執行兩次解鎖操作才能真正成功地讓當前執行執行緒釋放鎖。


4、重入鎖ReentrantLock的原始碼分析

4.1、獲取鎖和釋放鎖

先看下使用ReentrantLock加鎖的示例程式碼。

public void doSomething() {
    // 預設是獲取一個非公平鎖
    ReentrantLock lock = new ReentrantLock();
    try{
        // 執行前先加鎖
        lock.lock();   
        //執行操作...
    }finally{
        // 最後釋放鎖
        lock.unlock();
    }
}

以下是獲取鎖和釋放鎖這兩個操作的API:

// 獲取鎖的操作
public void lock() {
    sync.lock();
}
// 釋放鎖的操作
public void unlock() {
    sync.release(1);
}

可以看到獲取鎖和釋放鎖的操作分別委託給Sync物件的lock方法和release方法。

public class ReentrantLock implements Lock, java.io.Serializable {

    private final Sync sync;

    abstract static class Sync extends AbstractQueuedSynchronizer {
        abstract void lock();
    }

    // 實現非公平鎖的同步器
    static final class NonfairSync extends Sync {
        final void lock() {
            ...
        }
    }

    / /實現公平鎖的同步器
    static final class FairSync extends Sync {
        final void lock() {
            ...
        }
    }
}

每個ReentrantLock物件都持有一個Sync型別的引用,這個Sync類是一個抽象內部類它繼承自AbstractQueuedSynchronizer,它裡面的lock方法是一個抽象方法。ReentrantLock的成員變數sync是在構造時賦值的,下面我們看看ReentrantLock的兩個構造方法都做了些什麼?

// 預設無參構造器
public ReentrantLock() {
    sync = new NonfairSync();
}

// 有參構造器
public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}

呼叫預設無參構造器會將NonfairSync例項賦值給sync,此時鎖是非公平鎖,即Reentrant預設是非公平鎖。有參構造器允許通過引數來指定是將FairSync例項還是NonfairSync例項賦值給sync。

NonfairSync和FairSync都是繼承自Sync類並重寫了lock()方法,所以公平鎖和非公平鎖在獲取鎖的方式上有些區別,這個我們下面會講到。

再來看看釋放鎖的操作,每次呼叫unlock()方法都只是去執行sync.release(1)操作,這步操作會呼叫AbstractQueuedSynchronizer類的release()方法,我們再來回顧一下。

// 釋放鎖的操作(獨佔模式)
public final boolean release(int arg) {
    // 撥動密碼鎖, 看看是否能夠開鎖
    if (tryRelease(arg)) {
        // 獲取head節點
        Node h = head;
        // 如果head結點不為空並且等待狀態不等於0就去喚醒後繼節點
        if (h != null && h.waitStatus != 0) {
            // 喚醒後繼節點
            unparkSuccessor(h);
        }
        return true;
    }
    return false;
}

這個release()方法是AQS提供的釋放鎖操作的API,它首先會去呼叫tryRelease()方法去嘗試獲取鎖,tryRelease()方法是抽象方法,它的實現邏輯在子類Sync裡面。

abstract static class Sync extends AbstractQueuedSynchronizer {

    ...

    // 嘗試釋放鎖
    protected final boolean tryRelease(int releases) {
        int c = getState() - releases;
        // 如果持有鎖的執行緒不是當前執行緒就丟擲異常
        if (Thread.currentThread() != getExclusiveOwnerThread()) {
            throw new IllegalMonitorStateException();
        }
        boolean free = false;
        // 如果同步狀態為0則表明鎖被釋放
        if (c == 0) {
            // 設定鎖被釋放的標誌為真
            free = true;
            // 設定佔用執行緒為空
            setExclusiveOwnerThread(null);
        }
        setState(c);
        return free;
    }

    ...
}

這個tryRelease()方法首先會獲取當前同步狀態,並將當前同步狀態減去傳入的引數值得到新的同步狀態,然後判斷新的同步狀態是否等於0,如果等於0則表明當前鎖被釋放,然後先將鎖的釋放狀態置為真,再將當前佔有鎖的執行緒清空,最後呼叫setState()方法設定新的同步狀態並返回鎖的釋放狀態。

4.2、公平鎖和非公平鎖

我們知道ReentrantLock是公平鎖還是非公平鎖是基於sync指向的是哪個具體例項而決定的。

在ReentrantLock的建構函式中會為成員變數sync賦值:

// 預設無參構造器
public ReentrantLock() {
    sync = new NonfairSync();
}

// 有參構造器
public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}

如果賦值為NonfairSync例項則表明是非公平鎖,如果賦值為FairSync例項則表明為公平鎖。如果是公平鎖,執行緒將按照它們發出請求的順序來獲得鎖,但在非公平鎖上,則允許插隊行為:當一個執行緒請求非公平的鎖時,如果在發出請求的同時該鎖的狀態變為可用,那麼這個執行緒將跳過佇列中所有等待的執行緒直接獲得這個鎖。

下面我們先看看非公平鎖的獲取方式

// 非公平鎖的獲取
static final class NonfairSync extends Sync {
    // 實現父類的抽象獲取鎖的方法
    final void lock() {
        // 使用CAS方式設定同步狀態
        if (compareAndSetState(0, 1)) {
            // 如果設定成功則表明鎖沒被佔用
            setExclusiveOwnerThread(Thread.currentThread());
        } else {
            // 否則表明鎖已經被佔用, 呼叫acquire讓執行緒去同步佇列排隊獲取
            // acquire(1)是AQS類中的方法
            acquire(1);
        }
    }
    // 嘗試獲取鎖的方法
    protected final boolean tryAcquire(int acquires) {
        return nonfairTryAcquire(acquires);
    }
}

// 呼叫的是AQS中的方法:以不可中斷模式獲取鎖(獨佔模式)
public final void acquire(int arg) {
    if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) {
        selfInterrupt();
    }
}

可以看到在非公平鎖的 lock方法中,執行緒第一步就會以CAS方式將同步狀態的值從0改為1。其實這步操作就等於去嘗試獲取鎖,如果更改成功則表明執行緒剛來就獲取了鎖,而不必再去同步佇列裡面排隊了。如果更改失敗則表明執行緒剛來時鎖還未被釋放,所以接下來就呼叫AQS類中的acquire()方法。

我們知道這個acquire方法是繼承自AbstractQueuedSynchronizer的方法,現在再來回顧一下該方法,執行緒進入acquire方法後首先去呼叫tryAcquire方法嘗試去獲取鎖,由於NonfairSync覆蓋了tryAcquire方法,並在方法中呼叫了父類Sync的nonfairTryAcquire方法,所以這裡會呼叫到nonfairTryAcquire方法去嘗試獲取鎖。

我們看看這個nonfairTryAcquire()方法具體做了些什麼:

abstract static class Sync extends AbstractQueuedSynchronizer {

    ...

    // 非公平的獲取鎖
    final boolean nonfairTryAcquire(int acquires) {
        // 獲取當前執行緒
        final Thread current = Thread.currentThread();
        // 獲取當前同步狀態
        int c = getState();
        // 如果同步狀態為0則表明鎖沒有被佔用
        if (c == 0) {
            // 使用CAS更新同步狀態
            if (compareAndSetState(0, acquires)) {
                // 設定目前佔用鎖的執行緒
                setExclusiveOwnerThread(current);
                return true;
            }
        // 否則的話就判斷持有鎖的是否是當前執行緒
        }else if (current == getExclusiveOwnerThread()) {
            // 如果鎖是被當前執行緒持有的, 就直接修改當前同步狀態
            int nextc = c + acquires;
            if (nextc < 0) {
                throw new Error("Maximum lock count exceeded");
            }
            setState(nextc);
            return true;
        }
        // 如果持有鎖的不是當前執行緒則返回失敗標誌
        return false;
    }

    ...
}

nonfairTryAcquire()方法是Sync的方法,我們可以看到執行緒進入此方法後首先去獲取同步狀態,如果同步狀態為0就使用CAS操作更改同步狀態,其實這又是獲取了一遍鎖。如果同步狀態不為0表明鎖被佔用,此時會先去判斷持有鎖的執行緒是否是當前執行緒,如果是的話就將同步狀態加1,否則的話這次嘗試獲取鎖的操作宣告失敗。於是會呼叫addWaiter方法將執行緒新增到同步佇列。

綜上來看,在非公平鎖的模式下一個執行緒在進入同步佇列之前會嘗試獲取兩遍鎖,如果獲取成功則不進入同步佇列排隊,否則才進入同步佇列排隊。

接下來我們看看公平鎖的獲取方式

// 實現公平鎖的同步器
static final class FairSync extends Sync {
    // 實現父類的抽象獲取鎖的方法
    final void lock() {
        // 呼叫acquire讓執行緒去同步佇列排隊獲取
        acquire(1);
    }
    // 嘗試獲取鎖的方法
    protected final boolean tryAcquire(int acquires) {
        // 獲取當前執行緒
        final Thread current = Thread.currentThread();
        // 獲取當前同步狀態
        int c = getState();
        // 如果同步狀態0則表示鎖沒被佔用
        if (c == 0) {
            // 判斷同步佇列是否有前驅節點
            if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) {
                // 如果沒有前驅節點且設定同步狀態成功就表示獲取鎖成功
                setExclusiveOwnerThread(current);
                return true;
            }
        // 否則判斷是否是當前執行緒持有鎖
        }else if (current == getExclusiveOwnerThread()) {
            // 如果是當前執行緒持有鎖就直接修改同步狀態
            int nextc = c + acquires;
            if (nextc < 0) {
                throw new Error("Maximum lock count exceeded");
            }
            setState(nextc);
            return true;
        }
        // 如果不是當前執行緒持有鎖則獲取失敗
        return false;
    }
}

呼叫公平鎖的lock方法時會直接呼叫acquire方法。同樣的,acquire方法首先會呼叫FairSync重寫的tryAcquire方法來嘗試獲取鎖。在該方法中也是首先獲取同步狀態的值,如果同步狀態為0則表明此時鎖剛好被釋放,這時和非公平鎖不同的是它會先去呼叫 hasQueuedPredecessors() 方法查詢同步佇列中是否有人在排隊,如果沒人在排隊才會去修改同步狀態的值,可以看到公平鎖在這裡採取禮讓的方式而不是自己馬上去獲取鎖

除了這一步和非公平鎖不一樣之外,其他的操作都是一樣的。綜上所述,可以看到公平鎖在進入同步佇列之前只檢查了一遍鎖的狀態,即使是發現了鎖是開的也不會自己馬上去獲取,而是先讓同步佇列中的執行緒先獲取,所以可以保證在公平鎖下所有執行緒獲取鎖的順序都是先來後到的,這也保證了獲取鎖的公平性。

我們為什麼不希望所有鎖都是公平的呢?

畢竟公平是一種好的行為,而不公平是一種不好的行為。由於執行緒的掛起和喚醒操作存在較大的開銷而影響系統性能,特別是在競爭激烈的情況下公平鎖將導致執行緒頻繁的掛起和喚醒操作,而非公平鎖可以減少這樣的操作,所以在效能上將會優於公平鎖。

另外,由於大部分執行緒使用鎖的時間都是非常短暫的,而執行緒的喚醒操作會存在延時情況,有可能在A執行緒被喚醒期間B執行緒馬上獲取了鎖並使用完釋放了鎖,這就導致了雙贏的局面,A執行緒獲取鎖的時刻並沒有推遲,但B執行緒提前使用了鎖,並且吞吐量也獲得了提高。


5、等待佇列的實現機制

內建的等待佇列存在一些缺陷,每個內建鎖都只能有一個相關聯的等待佇列,這導致多個執行緒可能在同一個等待佇列上等待不同的條件,那麼每次呼叫notifyAll時都會將所有等待的執行緒喚醒,當執行緒醒來後發現並不是自己等待的條件,轉而又會被掛起。這導致做了很多無用的執行緒喚醒和掛起操作,而這些操作將會大量浪費系統資源,降低系統的效能。

如果想編寫一個帶有多個條件的併發物件,或者想獲得除了等待佇列可見性之外的更多控制權,就需要使用顯式的Lock和Condition而不是內建鎖和等待佇列。

一個Condition和一個Lock關聯在一起,就像一個等待佇列和一個內建鎖相關聯一樣。要建立一個Condition,可以在相關聯的Lock上呼叫Lock.newCondition方法。我們先來看一個使用Condition的示例。

public class BoundedBuffer {

    final Lock lock = new ReentrantLock();
    final Condition notFull = lock.newCondition();   //條件謂詞:notFull
    final Condition notEmpty = lock.newCondition();  //條件謂詞:notEmpty
    final Object[] items = new Object[100];
    int putptr, takeptr, count;

    // 生產方法
    public void put(Object x) throws InterruptedException {
        lock.lock();
        try {
            while (count == items.length)
                notFull.await();  // 佇列已滿, 執行緒在notFull佇列上等待
            items[putptr] = x;
            if (++putptr == items.length) putptr = 0;
            ++count;
            notEmpty.signal();    // 生產成功, 喚醒notEmpty佇列的結點
        } finally {
            lock.unlock();
        }
    }

    // 消費方法
    public Object take() throws InterruptedException {
        lock.lock();
        try {
            while (count == 0)
                notEmpty.await();  // 佇列為空, 執行緒在notEmpty佇列上等待
            Object x = items[takeptr];
            if (++takeptr == items.length) takeptr = 0;
            --count;
            notFull.signal();     // 消費成功, 喚醒notFull佇列的結點
            return x;
        } finally {
            lock.unlock();
        }
    } 
}

一個lock物件可以產生多個等待佇列,上訴案例裡產生了兩個等待佇列notFull和notEmpty。當容器已滿時再呼叫put方法的執行緒需要進行阻塞,等待條件為真(容器不滿)才醒來繼續執行;當容器為空時再呼叫take方法的執行緒也需要阻塞,等待條件為真(容器不空)才醒來繼續執行。

這兩類執行緒是根據不同的條件進行等待的,所以它們會進入兩個不同的條件佇列中阻塞,等到合適時機再通過呼叫Condition物件上的API進行喚醒。下面是newCondition方法的實現程式碼。

// 建立等待佇列
public Condition newCondition() {
    return sync.newCondition();
}

abstract static class Sync extends AbstractQueuedSynchronizer {
    
    ...    

    // 新建Condition物件
    final ConditionObject newCondition() {
        return new ConditionObject();
    }

    ...
}

ReentrantLock上的等待佇列的實現都是基於AbstractQueuedSynchronizer的,我們在呼叫newCondition方法時所獲得的Condition物件就是AQS的內部類ConditionObject的例項。所有對等待佇列的操作都是通過呼叫ConditionObject對外提供的API來完成的。

有關於ConditionObject的具體實現大家可以查閱我的這篇文章:佇列同步器AQS原始碼分析之Condition介面、等待佇列


  • AQS系列文章:

1、佇列同步器AQS原始碼分析之概要分析

2、佇列同步器AQS原始碼分析之獨佔模式

3、佇列同步器AQS原始碼分析之共享模式

4、佇列同步器AQS原始碼分析之Condition介面、等待佇列