(五)深入剖析并發之AQS獨占鎖&重入鎖(ReetrantLock)及Condition實現原理

引言

在我們前面的文章《深入理解Java并發編程之無鎖CAS機制》中我們曾提到的CAS機制如果說是整個Java并發編程基礎的話,那么本章跟大家所講述的AQS則是整個Java JUC的核心。不過在學習AQS之前需要對于CAS機制有一定的知識儲備,因為CAS在ReetrantLock及AQS中的實現隨處可見。

一、JUC中的Lock鎖接口

在我們并發編程的文章一開始,我們都是在圍繞著線程安全問題敘述它的解決方案,在前面的文章中我們曾提到過CAS無鎖機制、synchronized關鍵字等多種解決方案,在其中CAS機制屬于樂觀鎖類型,synchronized關鍵字屬于悲觀鎖類型,而我們本章要談到的基于AQS實現的ReetrantLock也是屬于悲觀鎖類型的實現。但是它與我們之前聊的synchronized并不相同,synchronized關鍵字屬于隱式鎖,鎖的獲取和釋放都是隱式的,且不需要開發人員干預。而我們本章要講的則是顯式鎖,即鎖的獲取和釋放都需要我們手動編碼實現。在JDK1.5時,官方在Java.uitl.concurrent并發包中添加了Lock鎖接口,該接口中定義了lock()[獲取鎖]和unlock()[釋放鎖]兩個方法對顯式鎖的加鎖與解鎖操作提供了支持。顯式鎖的使用方式如下:

Lock lock = new ReetrantLock(); //創建鎖對象
lock.lock(); //獲取鎖操作
try{
    //需要鎖修飾的代碼塊....
} finally{
    lock.unlock(); //釋放鎖操作
}

如上代碼在程序運行時,當前線程執行lock()方法之后,則代表著當前線程占用了鎖資源,在當前線程未執行unlock()方法之前,其他線程由于獲取不到鎖資源無法進入被鎖修飾的代碼塊執行,所以會一直被阻塞至當前線程釋放鎖時。不過我們在編碼過程中需要注意的是解鎖操作unlock()方法必須放入finally代碼塊中,這樣能夠確保即使加鎖代碼執行過程中拋出了異常線程最終也能釋放鎖資源,避免程序造成死鎖現象。當然Lock接口中除開定義了lock()與unlock()方法外,還提供了以下相關方法:

    /**
     * 獲取鎖:
     *     如果當前鎖資源空閑可用則獲取鎖資源返回,
     *     如果不可用則阻塞等待,不斷競爭鎖資源,直至獲取到鎖返回。
     */
    void lock();
    
    /**
     * 釋放鎖:
     *     當前線程執行完成業務后將鎖資源的狀態由占用改為可用并通知阻塞線程。
     */
    void unlock();

    /**
     * 獲取鎖:(與lock方法不同的在于可響應中斷操作,即在獲取鎖過程中可中斷)
     *     如果當前鎖資源可用則獲取鎖返回。
     *     如果當前鎖資源不可用則阻塞直至出現如下兩種情況:
     *        1.當前線程獲取到鎖資源。
     *        2.接收到中斷命令,當前線程中斷獲取鎖操作。
     */
    void lockInterruptibly() throws InterruptedException;

    /**
     * 非阻塞式獲取鎖:
     *    嘗試非阻塞式獲取鎖,調用該方法獲取鎖立即返回獲取結果。
     *    如果獲取到了鎖則返回true,反之返回flase。
     */
    boolean tryLock();

    /**
     * 非阻塞式獲取鎖:
     *   根據傳入的時間獲取鎖,如果線程在該時間段內未獲取到鎖返回flase。
     *   如果當前線程在該時間段內獲取到了鎖并未被中斷則返回true。
     */
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;


    /**
     * 獲取等待通知組件(該組件與當前鎖資源綁定):
     *    當前線程只有獲取到了鎖資源之后才能調用該組件的wait()方法,
     *    當前線程調用await()方法后,當前線程將會釋放鎖。
     */
    Condition newCondition();

通過分析如上Lock接口提供的方法可以得知,Lock鎖提供了很多synchronized鎖不具備的特性,如下:

  • ①獲取鎖中斷操作(synchronized關鍵字是不支持獲取鎖中斷的);
  • ②非阻塞式獲取鎖機制;
  • ③超時中斷獲取鎖機制;
  • ④多條件等待喚醒機制Condition等。

二、Lock接口的實現者:ReetrantLock重入鎖

ReetrantLock,JDK1.5時JUC包下添加的一個類,實現于Lock接口,作用與synchronized相同,不過對比于synchronized更加靈活,但是使用時需要我們手動獲取/釋放鎖。
ReetrantLock本身是支持重入的一把鎖,即支持當前獲取鎖的線程對鎖資源進行多次重復的鎖獲取,在此同時還支持公平鎖與非公平鎖。這里的公平與非公平指的是獲取鎖操作執行后鎖資源獲取的先后順序,如果先執行獲取鎖操作的線程先獲取鎖,那么就代表當前的鎖是公平的,反之,如果先執行獲取鎖操作的線程還需要和后面執行獲取鎖操作的線程競爭鎖資源,那么則代表當前鎖是非公平的。在這里值得注意的是:非公平鎖雖然會出現線程競爭鎖資源的情況,但是一般而言非公平鎖的效率在絕大部分情況下也遠遠超出公平鎖。不過在某些特殊的業務場景下,比如更加注重鎖資源獲取的先后順序,那么公平鎖才是最好的選擇。在前面我們也曾提到過ReetrantLock支持鎖重入即當前線程能夠多次執行獲取鎖操作,但是我們在使用ReetrantLock過程中要明白的是:ReetrantLock執行了幾次獲取鎖操作也需要執行多少次釋放鎖操作。案例如下:

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class Task implements Runnable {
    public static Lock lock = new ReentrantLock();
    public static int count = 0;

    @Override
    public void run() {
        for (int i = 0; i<10000;i++){
            lock.lock(); // 第一次獲取鎖
            lock.lock(); // 第二次獲取鎖
            try {
                count++; // 非原子性操作:存在線程安全問題
            } finally {
                lock.unlock(); // 第一次釋放鎖
                lock.unlock(); // 第二次釋放鎖
            }
        }
    }
    
    public static void main(String[] args) throws InterruptedException {
        Task task = new Task();
        Thread t1 = new Thread(task);
        Thread t2 = new Thread(task);
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(count);
        // 執行結果:20000
    }
}

上面的這個例子很簡單,t1,t2兩個線程同時對共享資源count進行++的非原子性操作,我們在這里使用ReentrantLock鎖解決存在的線程安全問題。同時我們在上述代碼中,獲取了兩次鎖資源,因為ReentrantLock支持鎖重入,所以此時獲取兩次鎖是沒有問題的,不過在finally中執行釋放鎖資源時需要注意:也應該執行兩次unlock釋放鎖的操作。從上述案例中分析我們可以發現,其實ReentrantLock的用法相對來說比較簡單,我們接下來也可以分析一下ReentrantLock所提供的一些方法以便于更加全面的認識它。如下:

// 查詢當前線程調用lock()的次數
int getHoldCount() 

// 返回目前持有此鎖的線程,如果此鎖不被任何線程持有,返回null  
protected Thread getOwner(); 

// 返回一個集合,它包含可能正等待獲取此鎖的線程,其內部維持一個隊列(后續分析)
protected Collection<Thread> getQueuedThreads(); 

// 返回正等待獲取此鎖資源的線程估計數
int getQueueLength();

// 返回一個集合,它包含可能正在等待與此鎖相關的Condition條件的線程(估計值)
protected Collection<Thread> getWaitingThreads(Condition condition); 

// 返回調用當前鎖資源Condition對象await方法后未執行signal()方法的線程估計數
int getWaitQueueLength(Condition condition);

// 查詢指定的線程是否正在等待獲取當前鎖資源
boolean hasQueuedThread(Thread thread); 

// 查詢是否有線程正在等待獲取當前鎖資源
boolean hasQueuedThreads();

// 查詢是否有線程正在等待與此鎖相關的Condition條件
boolean hasWaiters(Condition condition); 

// 返回當前鎖類型,如果是公平鎖返回true,反之則返回flase
boolean isFair() 

// 查詢當前線程是持有當前鎖資源
boolean isHeldByCurrentThread() 

// 查詢當前鎖資源是否被線程持有
boolean isLocked()

通過觀察我們不難得知,ReentrantLock作為Lock接口的實現者,除開在實現了Lock接口定義的方法外,ReentrantLock也拓展了一些其他方法。我們可以通過一個簡單的案例來熟悉一下ReentrantLock一些其他方法的作用。案例如下:

import lombok.SneakyThrows;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;

public class Task implements Runnable {

    public static ReentrantLock lock = new ReentrantLock();
    public static int count = 0;

    // ReentrantLock的簡單使用案例
    @SneakyThrows
    @Override
    public void run() {
        for (int i = 0; i < 10000; i++) {
            lock.lock(); // 第一次阻塞式獲取鎖
            lock.tryLock(); // 第二次非阻塞式獲取鎖
            lock.tryLock(10,TimeUnit.SECONDS); // 第三次非阻塞等待式獲取鎖
            try {
                count++; // 非原子性操作:存在線程安全問題
            } finally {
                lock.unlock(); // 第一次釋放鎖
                lock.unlock(); // 第二次釋放鎖
                lock.unlock(); // 第三次釋放鎖
            }
        }
    }

    public void reentrantLockApiTest() {
        lock.lock(); // 獲取鎖
        try {
            //獲取當前線程調用lock()方法的次數
            System.out.println("線程:" + Thread.currentThread().getName() + "\t調用lock()次數:" + lock.getHoldCount());
            // 判斷當前鎖是否為公平鎖
            System.out.println("當前鎖資源類型是否為公平鎖?" + lock.isFair());
            // 獲取等待獲取當前鎖資源的估計線程數
            System.out.println("目前有:" + lock.getQueueLength() + "個線程正在等待獲取鎖資源!");
            // 指定線程是否在等待獲取當前鎖資源
            System.out.println("當前線程是否在等待獲取當前鎖資源?" + lock.hasQueuedThread(Thread.currentThread()));
            // 判斷當前鎖資源是否有線程在等待獲取
            System.out.println("當前鎖資源是否存在線程等待獲取?" + lock.hasQueuedThreads());
            // 判斷當前線程是否持有當前鎖資源
            System.out.println("當前線程是否持有當前鎖資源?" + lock.isHeldByCurrentThread());
            // 判斷當前鎖資源是否被線程持有
            System.out.println("當前鎖資源是否被線程占用?" + lock.isLocked());
        } finally {
            lock.unlock(); // 釋放鎖
        }
    }


    public static void main(String[] args) throws InterruptedException {
        Task task = new Task();
        Thread t1 = new Thread(task);
        Thread t2 = new Thread(task);
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(count); // 執行結果:20000
        /**
         * 執行結果:
         *   線程:main    調用lock()次數:1
         *   當前鎖資源類型是否為公平鎖?false
         *   目前有:0個線程正在等待獲取鎖資源!
         *   當前線程是否在等待獲取當前鎖資源?false
         *   當前鎖資源是否存在線程等待獲???false
         *   當前線程是否持有當前鎖資源?true
         *   當前鎖資源是否被線程占用?true
         */
        task.reentrantLockApiTest();
    }
}

通過上面的簡單案例我們可以看到ReentrantLock鎖的使用還是比較簡單的,所以我們關于ReentrantLock的應用暫時先告一段落,接下來我們一步步的帶著大家分析去ReentrantLock內部實現原理,其實ReentrantLock是基于AQS框架實現的,所以在研究ReentrantLock內部實現之前我們先帶大家深入了解一下AQS。

三、JUC并發包內核:并發基礎組件AQS

AQS全稱為AbstractQueuedSynchronizer(抽象的隊列同步器),Java并發包中的核心基礎組件,它是用來構建信號量、鎖、門閥等其他同步組件的基礎框架。

AQS工作原理簡述

在之前的《徹底理解Java并發編程之Synchronized關鍵字實現原理剖析》中談到過,synchronized重量級鎖底層的實現是基于ObjectMonitor對象中的計數器實現的,而在AQS中也存在著異曲同工之處,它內部通過一個用volatile關鍵字修飾的int類型全局變量state作為標識來控制同步狀態。當狀態標識state為0時,代表著當前沒有線程占用鎖資源,反之當狀態標識state不為0時,代表著鎖資源已經被線程持有,其他想要獲取鎖資源的線程必須進入同步隊列等待當前持有鎖的線程釋放。AQS通過內部類Node構建FIFO(先進先出)的同步隊列用來處理未獲取到鎖資源的線程,將等待獲取鎖資源的線程加入到同步隊列中進行排隊等待。同時AQS使用內部類ConditionObject用來構建等待隊列,當Condition調用await()方法后,等待獲取鎖資源的線程將會加入等待隊列中,而當Condition調用signal()方法后,線程將從等待隊列轉移到同步隊列中進行鎖資源的競爭。值得我們注意的是在這里存在兩種類型的隊列:
①同步隊列:當線程獲取鎖資源發現已經被其他線程占有而加入的隊列;
②等待隊列(可能存在多個):當Condition調用await()方法后加入的隊列;
大家在理解時不可將兩者混為一談。我們可以首先分析一下AQS中的同步隊列,AQS同步隊列模型如下:

public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer{
// 指向同步隊列的頭部
private transient volatile Node head;
// 指向同步隊列的尾部
private transient volatile Node tail;
// 同步狀態標識
private volatile int state;
// 省略......
}

其中head以及tail是AQS的全局變量,其中head指向同步隊列的頭部,但是需要注意的是head節點為空不存儲信息,而tail指向同步隊列的尾部。AQS中同步隊列采用這種方式構建雙向鏈表結構方便隊列進行節點增刪操作。state則為我們前面所提到的同步狀態標識,當線程在執行過程中調用獲取鎖的lock()方法后,如果state=0,則說明當前鎖資源未被其他線程獲取,當前線程將state值設置為1,表示獲取鎖成功。如果state=1,則說明當前鎖資源已被其他線程獲取,那么當前線程則會被封裝成Node節點加入同步隊列進行等待。Node節點是對每一個獲取鎖資源線程的封裝體,其中包括了當前執行的線程本身以及線程的狀態,如是否被阻塞、是否處于等待喚醒、是否中斷等。每個Node節點中都關聯著前驅節點prev以及后繼節點next,這樣能夠方便持有鎖的線程釋放后能快速釋放下一個正在等待的線程。Node類結構如下:

static final class Node {
    // 共享模式
    static final Node SHARED = new Node();
    // 獨占模式
    static final Node EXCLUSIVE = null;
    // 標識線程已處于結束狀態
    static final int CANCELLED =  1;
    // 等待被喚醒狀態
    static final int SIGNAL    = -1;
    // Condition條件狀態
    static final int CONDITION = -2;
    // 在共享模式中使用表示獲得的同步狀態會被傳播
    static final int PROPAGATE = -3;

    // 等待狀態,存在CANCELLED、SIGNAL、CONDITION、PROPAGATE四種
    volatile int waitStatus;

    // 同步隊列中前驅結點
    volatile Node prev;

    // 同步隊列中后繼結點
    volatile Node next;

    // 獲取鎖資源的線程
    volatile Thread thread;

    // 等待隊列中的后繼結點(與Condition有關,稍后會分析)
    Node nextWaiter;

    // 判斷是否為共享模式
    final boolean isShared() {
        return nextWaiter == SHARED;
    }
    // 獲取前驅結點
    final Node predecessor() throws NullPointerException {
        Node p = prev;
        if (p == null)
            throw new NullPointerException();
        else
            return p;
    }
    // 省略代碼.....
}

在其中SHARED和EXCLUSIVE兩個全局常量分別代表著共享模式和獨占模式,共享模式即允許多個線程同時對一個鎖資源進行操作,例如:信號量Semaphore、讀鎖ReadLock等采用的就是基于AQS的共享模式實現的。而獨占模式則代表著在同一時刻只運行一個線程對鎖資源進行操作,如ReentranLock等組件的實現都是基于AQS的獨占模式實現。全局變量waitStatus則代表著當前被封裝成Node節點的線程的狀態,一共存在五種情況:

  • 0 初始值狀態:waitStatus=0,代表節點初始化。
  • CANCELLED 取消狀態:waitStatus=1,在同步隊列中等待的線程等待超時或者被中斷,需要從同步隊列中取消該Node的節點,其節點的waitStatus為CANCELLED,進入該狀態后的節點代表著進入了結束狀態,當前節點將不會再發生變化。
  • SIGNAL 信號狀態:waitStatus=-1,被標識為該狀態的節點,當其前驅節點的線程釋放了鎖資源或被取消,將會通知該節點的線程執行。簡單來說被標記為當前狀態的節點處于等待喚醒狀態,只要前驅節點釋放鎖,就會通知標識為SIGNAL狀態的后續節點的線程執行。
  • CONDITION 條件狀態:waitStatus=-2,與Condition相關,被表示為該狀態的節點處于等待隊列中,節點的線程等待在Condition條件,當其他線程調用了Condition的signal()方法后,CONDITION狀態的節點將從等待隊列轉移到同步隊列中,等待獲取競爭鎖資源。
  • PROPAGATE 傳播狀態:waitStatus=-3,該狀態與共享模式有關,在共享模式中,被標識為該狀態的節點的線程處于可運行狀態。

Node節點結構

全局變量pre和next分別代表著當前Node節點對應的前驅節點和后繼節點,thread代表當前被封裝的線程對象。nextWaiter代表著等待隊列中,當前節點的后繼節點(與Condition有關稍后分析)。到這里其實我們對于Node數據類型的結構有了大概的了解了??傊珹QS作為JUC的核心組件,對于鎖存在兩種不同的實現,即獨占模式(如ReetrantLock)與共享模式(如Semaphore)。但是不管是獨占模式還是共享模式的實現類,都是建立在AQS的基礎上實現,其內部都維持著一個隊列,當試圖獲取鎖的線程數量超過當前模式限制時則會將線程封裝成一個Node節點加入隊列進行等待。而這一系列操作都是由AQS幫我們完成,無論是ReetrantLock還是Semaphore,其實它們的絕大部分方法最終都是直接或間接的調用AQS完成的。下面是AQS整體類圖結構:
AQS整體類圖結構

  • AbstractOwnableSynchronizer抽象類: 內部定義了存儲當前持有鎖資源線程以及獲取存儲線程信息方法。
  • AbstractQueuedSynchronizer抽象類: AQS指的就是AbstractQueuedSynchronizer的首字母縮寫,整個AQS框架的核心類。內部以虛擬隊列的形式實現了線程對于鎖資源獲取(tryAcquire)與釋放(tryRelease),但是在AQS中沒有對鎖獲取與鎖釋放的操作進行默認實現,具體的邏輯需要子類實現,這樣使得我們在開發過程中能夠更加靈活的運用它。
  • Node內部類: AbstractQueuedSynchronizer中的內部類,用于構建AQS內部的虛擬隊列,方便于AQS管理需要獲取鎖的線程。
  • Sync內部抽象類: ReentrantLock的內部類,繼承AbstractQueuedSynchronizer類并實現了其定義的鎖資源獲取(tryAcquire)與釋放(tryRelease)方法,同時也定義了lock()方法,提供給子類實現。
  • NonfairSync內部類: ReentrantLock的內部類,繼承Sync類,非公平鎖的實現者。
  • FairSync內部類: ReentrantLock的內部類,繼承Sync類,公平鎖的實現者。
  • Lock接口: Java鎖類的頂級接口,定義了一系列鎖操作的方法,如:lock()、unlock()、tryLock等。
  • ReentrantLock: Lock鎖接口的實現者,內部存在Sync、NonfairSync、FairSync三個內部類,在創建時可以根據其內部fair參數決定使用公平鎖/非公平鎖,其內部操作絕大部分都是基于間接調用AQS方法完成。

我們可以通過上面類圖關系看出AQS是一個抽象類,但是在其源碼實現中并不存在任何抽象方法,這是因為AQS設計的初衷更傾向于作為一個基礎組件,并不希望直接作為操作類對外輸出,為真正的實現類提供基礎設施,如構建同步隊列,控制同步狀態等。從設計模式角度來看,AQS采用的模板模式的模式構建的,其內部除了提供并發操作核心方法以及同步隊列操作外,還提供了一些模板方法讓子類自己實現,如加鎖操作及解鎖操作,為什么這么做呢?這是因為AQS作為基礎組件,封裝的是核心并發操作,但是實現上分為兩種模式,即共享模式與獨占模式,而這兩種模式的加鎖與解鎖實現方式是不一樣的,但AQS只關注內部公共方法實現并不關心外部不同模式的具體邏輯實現,所以提供了模板方法給子類使用,也就是說實現獨占鎖,如ReentrantLock需要自己實現tryAcquire()方法和tryRelease()方法,而實現共享模式的Semaphore,則需要實現tryAcquireShared()方法和tryReleaseShared()方法,這樣做的好處是顯而易見,無論是共享模式還是獨占模式,其基礎的實現都是同一套組件(AQS),只不過加鎖/解鎖的邏輯不同,更重要的是如果我們需要自定義鎖的話,也變得非常簡單,只需要選擇不同的模式實現不同的加鎖和解鎖的模板方法即可,AQS提供給獨占模式和共享模式的模板方法如下:

//獨占模式下獲取鎖的方法
protected boolean tryAcquire(int arg) {
    throw new UnsupportedOperationException();
}
//獨占模式下釋放鎖的方法
protected boolean tryRelease(int arg) {
    throw new UnsupportedOperationException();
}
//共享模式下獲取鎖的方法
protected int tryAcquireShared(int arg) {
    throw new UnsupportedOperationException();
}
//共享模式下釋放鎖的方法
protected boolean tryReleaseShared(int arg) {
    throw new UnsupportedOperationException();
}
//判斷是否持有獨占鎖的方法
protected boolean isHeldExclusively() {
    throw new UnsupportedOperationException();
}

到此我們對于AQS這個并發核心組件的原理大致有了一定了解,接下來我們會帶著大家基于ReetrantLock進一步分析AQS的具體實現過程。

四、基于ReetrantLock分析AQS獨占模式實現過程及原理

4.1、ReetrantLock中的NonfairSync非公平鎖

AQS同步器對于同步狀態標識state的管理是基于其內部FIFO雙向鏈表的同步隊列實現的。當一條線程獲取鎖失敗時,AQS同步器會將該線程本身及其相關信息封裝成Node節點加入同步隊列,同時也會阻塞當前線程,直至同步狀態標識state被釋放時,AQS才會將同步隊列中頭節點head內的線程喚醒,讓其嘗試修改state標識獲取鎖。下面我們重點來分析一下獲取鎖、釋放鎖以及將線程封裝成節點加入隊列的具體邏輯,這里先從ReetrantLock非公平鎖的角度入手分析AQS的具體實現。

// 構造函數:默認創建的鎖屬于非公平鎖(NonfairSync)類型
public ReentrantLock() {
    sync = new NonfairSync();
}
// 構造函數:根據傳入參數創建鎖類型(true公平鎖/false非公平鎖)
public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}
// 加鎖/獲取鎖操作
public void lock() {
     sync.lock();
}

4.1.1、ReetrantLock中獲取鎖lock()方法原理分析

我們先從非公平鎖的角度開始分析:

/**
 * 非公平鎖類<Sync子類>
 */
static final class NonfairSync extends Sync {
    // 加鎖
    final void lock() {
        // 執行CAS操作,修改同步狀態標識獲取鎖資源
        // 因為存在多條線程同時修改的可能,所以需要用CAS操作保證原子性
        if (compareAndSetState(0, 1))
            // 成功則將獨占鎖線程設置為當前線程  
            setExclusiveOwnerThread(Thread.currentThread());
        else acquire(1); // 否則再次請求同步狀態
    }
}

在NonfairSync類中對于獲取鎖的實現過程大概如下:首先對state進行cas操作嘗試將同步狀態標識從0修改為1.如果成功則返回true,代表成功獲取同步狀態,獲取鎖資源成功,之后再將獨占鎖線程設置為當前獲取同步狀態的線程。反之,如果為false則代表獲取鎖失敗,當返回false時執行acquire(1)方法,該方法對于線程中斷操作不敏感,代表著即使當前線程獲取鎖失敗被加入同步隊列等待,后續對當前線程執行中斷操作,當前線程也不會從同步隊列中移出。acquire(1)如下:

public final void acquire(int arg) {
    // 再次嘗試獲取同步狀態
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

acquire()是AQS中提供的方法,這里傳入參數arg代表著獲取同步狀態后設置的值(即要設置state的值,而state為0時是鎖資源釋放狀態,1則是鎖資源占用狀態),因為要獲取鎖,所以這里一般傳遞參數為1,進入方法后首先會執行tryAcquire(arg)方法,在前面的分析中我們發現AQS是將該方法交由子類實現的,因此NonfairSync的tryAcquire(arg)方法是由ReetrantLock類內部Sync類實現。代碼如下:

// NonfairSync類
static final class NonfairSync extends Sync {
    protected final boolean tryAcquire(int acquires) {
         return nonfairTryAcquire(acquires);
     }
 }

// ReetrantLock類內部類 - Sync類
abstract static class Sync extends AbstractQueuedSynchronizer {
  // NonfairTryAcquire方法
  final boolean nonfairTryAcquire(int acquires) {
      // 獲取當前執行線程及當前同步器的狀態標識值
      final Thread current = Thread.currentThread();
      int c = getState();
      // 判斷同步狀態是否為0,并嘗試再次獲取同步狀態
      if (c == 0) {
          //執行CAS操作嘗試修改同步標識
          if (compareAndSetState(0, acquires)) {
              // 如果為true則將獨占鎖線程設置為當前線程
              setExclusiveOwnerThread(current);
              return true;
          }
      }
      // 如果當前線程已獲取鎖,屬于重入鎖,再次獲取鎖后將state值加1
      else if (current == getExclusiveOwnerThread()) {
          // 對當前state值進行自增
          int nextc = c + acquires;
          if (nextc < 0) // overflow
              throw new Error("Maximum lock count exceeded");
          // 設置當前同步狀態,當前只有一個線程持有鎖,因為不會發生線程安全問
          // 題,可以直接執行 setState(nextc);
          setState(nextc);
          return true;
      }
      return false;
  }
  //省略......
}

分析如上代碼我們可以從中得知,在非公平鎖的nonfairTryAcquire(acquires)方法中做了兩件事:

  • 一、嘗試重新修改同步標識獲取鎖資源(因為可能存在上個獲取鎖的線程在當前線程上次獲取鎖失敗到目前這段時間之前釋放了鎖),成功則將獨占鎖線程設置為當前獲取同步狀態的線程,最后返回ture。
  • 二、判斷當前線程current是否為獨占鎖線程OwnerThread,如果是則代表著當前線程已經獲取過鎖資源還未釋放,屬于鎖重入,那么對state進行自增1,返回true。
  • 如果當前線程前面兩個判斷都不滿足,則返回false,也就代表著nonfairTryAcquire(acquires)執行結束。

不過在這個方法中值得注意的是,nonfairTryAcquire(acquires)方法中修改state同步標識時使用的是cas操作保證線程安全,因此只要任意一個線程調用nonfairTryAcquire(acquires)方法并設置成功即可獲取鎖,不管該線程是新到來的還是已在同步隊列的線程,畢竟這是非公平鎖,并不保證同步隊列中的線程一定比新到來線程請求(可能是head結點剛釋放同步狀態然后新到來的線程恰好獲取到同步狀態)先獲取到鎖,這點跟后面還會分析的公平鎖不同。那么我們再次回到之前NonfairSync類中的lock()方法中調用的acquire(1)方法:

public final void acquire(int arg) {
    // 再次嘗試獲取同步狀態
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

在這里,如果tryAcquire(arg)執行后能夠成功獲取鎖返回true,這個if自然不用繼續往下執行,這是最理想的狀態。但是如果當tryAcquire(arg)返回false時,則會繼續執行addWaiter(Node.EXCLUSIVE)封裝線程入列操作(因為ReetrantLock屬于獨占式鎖,所以Node節點類型屬于Node.EXCLUSIVE)。addWaiter方法代碼如下:

private Node addWaiter(Node mode) {
    // 將請求同步狀態失敗的線程封裝成Node節點
    Node node = new Node(Thread.currentThread(), mode);

    Node pred = tail;
    // 如果是第一個節點加入肯定為空,跳過。
    // 如果不是第一個節點則直接執行CAS入隊操作,嘗試在尾部快速添加
    if (pred != null) {
        node.prev = pred;
        // 使用CAS執行尾部節點替換,嘗試在尾部快速添加
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    // 如果第一次加入或者CAS操作沒有成功執行enq入隊操作
    enq(node);
    return node;
}

addWaiter()方法中,首先將當前線程和傳入的節點類型Node.EXCLUSIVE封裝成了一個Node節點,然后將AQS中的全局變量tail(指向AQS內部維護的同步隊列隊尾的節點)賦值給了pred用于判斷,如果隊尾節點不為空,則代表同步隊列中已經存在節點,直接嘗試執行CAS操作將當前封裝的Node快速追加到隊列尾部,如果CAS失敗則執行enq(node)方法。當然,如果在判斷時,tail節點為空,也就代表著同步隊列中還沒有任何節點存在,那么也會直接執行enq(node)方法。我們接著繼續分析enq(node)函數的實現:

private Node enq(final Node node) {
    // 死循環
    for (;;) {
         Node t = tail;
         // 如果隊列為null,即沒有頭結點
         if (t == null) { // Must initialize
             // 創建并使用CAS設置頭結點
             if (compareAndSetHead(new Node()))
                 tail = head;
         } else { // 隊尾添加新結點
             node.prev = t;
             if (compareAndSetTail(t, node)) {
                 t.next = node;
                 return t;
             }
         }
     }
}

在這個方法中使用了for(;;)開始了一個死循環并在其內進行CAS操作(可以避免并發問題出現)。在其中做了兩件事情:一是如果AQS內部的同步隊列還沒有初始化則創建一個新的節點然后再調用compareAndSetHead()方法將該節點設置為頭節點;二是如果同步隊列已經存在的情況下則將傳遞進來的節點快速添加到隊尾。注意這兩個步驟都存在同一時間內多條線程一同操作的可能,如果有一條線程修改head和tail成功,那么其他線程將繼續循環,直到修改成功,這里使用CAS原子操作進行頭節點head設置和尾節點tail替換,可以保證線程安全。同時從這里也可以看出head節點本身不存任何數據,僅僅只是一個new出來的Node節點,它只是作為一個牽頭節點,而tail永遠指向尾部節點(前提是隊列不為null)。

例:線程T1,T2,T3,T4,T5,T6六條線程同時進行入隊操作,但是只有T2入隊成功,其他五條線程(T1,T3,T4,T5,T6)將會繼續循環直至入隊成功為止。

添加到同步隊列的節點都會進入一個自旋過程,每個節點都在觀察時機等待條件滿足時,開始獲取同步狀態,然后從同步隊列中退出并結束自旋,回到之前的acquire()方法,自旋過程是在acquireQueued(addWaiter(Node.EXCLUSIVE),arg))方法中執行的,代碼如下:

final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;
    try {
        boolean interrupted = false; // 阻塞掛起標識
        // 一個死循環自旋
        for (;;) {
            // 獲取前驅節點
            final Node p = node.predecessor();
            // 如果p為頭節點才嘗試獲取同步狀態
            if (p == head && tryAcquire(arg)) {
                // 將node設置為頭節點
                setHead(node);
                // 將原有的head節點設置為null方便GC
                p.next = null; // help GC
                failed = false;
                return interrupted;
            }
            // 如果前驅節點不是head,判斷是否阻塞掛起線程
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            // 如果最終都沒能成功獲取同步狀態,結束該線程的請求
            cancelAcquire(node);
    }
}

當前節點中的線程在死循環(自旋)執行過程中,當節點的前驅節點為頭節點時開始嘗試獲取同步狀態(符合FIFO原則)。head節點是當前占有同步狀態標識的線程節點,只有當head節點釋放同步狀態喚醒后繼節點時,后繼節點才可能獲取同步狀態,所以這也是為什么說:只有當節點的前驅節點為頭節點時才開始嘗試獲取同步狀態的原因,在此之外的其他時候將被掛起。如果當前節點已經開始嘗試獲取同步狀態,進入if后則會執行setHead()方法將當前線程設置為head節點,如下:

// 將傳遞的節點設置為同步隊列的頭節點
private void setHead(Node node) {
    head = node;
    // 清空當前節點存儲的數據信息
    node.thread = null;
    node.prev = null;
}

node節點被設置為head頭節點后,當前節點存儲的線程以及前驅節點信息將會清空,因為當前線程已經成功獲取到了鎖資源,沒有必要再存儲線程信息,同時因為當前節點已經成為了頭節點,不存在前驅節點了,所以也會被清空信息。head節點只保留指向后繼節點的信息方便當前節點釋放鎖資源時喚醒后繼線程。如上則是節點的前驅節點為頭節點時會執行的邏輯,如果節點的前驅節點并不是head則會執行if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) interrupted = true;邏輯,代碼如下:

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    // 獲取當前節點的等待狀態
    int ws = pred.waitStatus;
    // 如果為等待喚醒(SIGNAL)狀態則返回true
    if (ws == Node.SIGNAL)
        return true;
    // 如果當前節點等待狀態大于0則說明是結束狀態,
    // 遍歷前驅節點直到找到沒有結束狀態的節點
    if (ws > 0) {
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else {
        // 如果當前節點等待狀態小于0又不是SIGNAL狀態,
        // 則將其設置為SIGNAL狀態,代表該節點的線程正在等待喚醒
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}

private final boolean parkAndCheckInterrupt() {
    // 將當前線程掛起
    LockSupport.park(this);
    // 獲取線程中斷狀態,interrupted()是判斷當前中斷狀態,
    // 而并不是中斷線程,因此結果可能是true也可能false并返回
    return Thread.interrupted();
}

LockSupport → park()方法:
public static void park(Object blocker) {
    Thread t = Thread.currentThread();
    // 設置當前線程的監視器blocker
    setBlocker(t, blocker);
    // 調用了native方法到JVM級別的阻塞機制阻塞當前線程
    UNSAFE.park(false, 0L);
    // 阻塞結束后把blocker置空
    setBlocker(t, null);
}

shouldParkAfterFailedAcquire()方法的作用是判斷節點的前驅節點是否為等待喚醒狀態(SIGNAL狀態),如果是則返回true。如果前驅節點的waitStatus大于0(只有CANCELLED結束狀態=1>0),既代表該前驅結點已沒有用了,應該從同步隊列移除,執行do/while循環遍歷所有前驅節點,直到尋找到非CANCELLED狀態的節點。但是如果當前節點的前驅節點的waitStatus不為CANCELLED結束狀態,也不為SIGNAL等待喚醒狀態,也就是代表節點是剛從Condition的條件等待隊列轉移到同步隊列,結點狀態為CONDITION狀態,因此需要轉換為SIGNAL狀態,那么則將其轉換為SIGNAL狀態,等待被喚醒。
shouldParkAfterFailedAcquire()方法返回true則代表著當前節點的前驅節點為(SIGNAL等待喚醒狀態,但是該前驅節點又不是head頭節點時,則使用parkAndCheckInterrupt()掛起線程,然后將節點狀態改變為WAITING狀態。當節點狀態為WAITING狀態時則需要等待unpark()操作來喚醒它,到此ReetrantLock內部間接通過AQS的FIFO的同步隊列就完成了lock()加鎖操作,下面我們可以總結一下整體的流程圖:

tryAcquire(arg)執行過程

AQS之圖解獨占式獲取鎖過程

4.1.2、ReetrantLock中一些其他獲取鎖資源方法的原理

在前面已經帶著大家詳細的談到了ReetrantLock.lock()方法的具體實現原理了,那么我們在開發過程中,有時還會用到可中斷的獲取方式加鎖,例如調用ReetrantLock的lockInterruptibly()、tryLock(),那么這些方法最終底層都會間接的調用到doAcquireInterruptibly()方法。如下:

 private void doAcquireInterruptibly(int arg)
    throws InterruptedException {
    // 封裝一個Node節點嘗試入隊操作
    final Node node = addWaiter(Node.EXCLUSIVE);
    boolean failed = true;
    try {
        for (;;) {
            // 獲取當前節點的前驅節點
            final Node p = node.predecessor();
            // 如果前驅節點為head節點則嘗試獲取鎖資源/同步狀態標識
            if (p == head && tryAcquire(arg)) {
                // 獲取成功后將當前節點設置成head節點
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return;
            }
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                // 直接拋異常,中斷線程的同步狀態請求
                throw new InterruptedException();
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

與lock()方法的區別在于:

/** ---------------lock()--------------- */
// 如果前驅節點不是head,判斷是否阻塞掛起線程
if (shouldParkAfterFailedAcquire(p, node) &&
    parkAndCheckInterrupt())
    interrupted = true;
    
/** --------lockInterruptibly()、tryLock()------- */
if (shouldParkAfterFailedAcquire(p, node) &&
    parkAndCheckInterrupt())
    // 直接拋異常,中斷線程的同步狀態請求
    throw new InterruptedException();

在可中斷式獲取鎖資源的方式中,當檢測到線程的中斷操作后,直接拋出異常,從而中斷線程的同步狀態請求,移除同步隊列。

4.1.3、ReetrantLock中的unlock()釋放鎖原理分析

一般而言,我們在使用ReetrantLock這類顯式鎖時,獲取鎖之后也需要我們手動釋放鎖資源。在ReetrantLock中當你調用了lock()獲取鎖資源之后,也需要我們手動調用unlock()釋放鎖。unlock()釋放鎖的代碼如下:

// ReetrantLock → unlock()方法
public void unlock() {
    sync.release(1);
}

// AQS → release()方法
public final boolean release(int arg) {
    // 嘗試釋放鎖
    if (tryRelease(arg)) {
        // 獲取頭結點用于判斷
        Node h = head;
        if (h != null && h.waitStatus != 0)
            // 喚醒后繼節點的線程
            unparkSuccessor(h);
        return true;
    }
    return false;
}

// ReentrantLock → Sync → tryRelease(int releases)方法
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;
      // 設置Owner為null
      setExclusiveOwnerThread(null);
  }
  // 設置更新同步狀態
  setState(c);
  return free;
}

釋放鎖的邏輯相對與獲取鎖的邏輯來說要簡單許多,unlock()方法最終是調用tryRelease(int releases)釋放鎖的,而tryRelease(int releases)則是ReetrantLock實現的方法,因為在AQS中沒有提供具體實現,而是交由了子類自己實現具體的邏輯。釋放鎖資源后會使用unparkSuccessor(h)喚醒后繼節點的線程。unparkSuccessor(h)的代碼如下:

private void unparkSuccessor(Node node) {
    // node一般為當前線程所在的節點,獲取當前線程的等待狀態
    int ws = node.waitStatus;
    if (ws < 0) // 置零當前線程所在的節點狀態,允許失敗
        compareAndSetWaitStatus(node, ws, 0);

    Node s = node.next; // 獲取當前節點的后繼節點
    if (s == null || s.waitStatus > 0) { // 如果為空或已結束
        s = null;
        for (Node t = tail; t != null && t != node; t = t.prev)
            // 等待狀態<=0的節點,代表是還有效的節點
            if (t.waitStatus <= 0)
                s = t;
    }
    if (s != null)
        LockSupport.unpark(s.thread); // 喚醒后繼節點線程
}

unparkSuccessor(h)方法中,最終是通過unpark()方法喚醒后繼節點中未放棄競爭鎖資源的線程,也就是waitStatus<=0的節點s,在我們前面分析獲取鎖原理時,曾分析到一個自旋的方法acquireQueued(),我們現在可以結合起來一同理解。s節點的線程被喚醒后,會執行acquireQueued()方法中的代碼if (p == head && tryAcquire(arg))進行判斷操作(就算p不為head頭結點也不會有影響,因為會執行shouldParkAfterFailedAcquire()方法),當前持有鎖資源的線程所在的節點node釋放之后,s經過unparkSuccessor()方法的邏輯處理之后,s便成為了AQS同步隊列中最前端的未放棄鎖資源競爭的線程,那最終經過shouldParkAfterFailedAcquire()方法邏輯處理之后,s節點也會成為head頭結點的next節點。所以最終在自旋方法中,第二次循環到if (p == head && tryAcquire(arg))邏輯時p==head的判斷式也就會成立了,然后s會將自己設置為head頭結點表示自己已經獲取到了鎖資源,最后整個acquire()方法執行結束。

總而言之,在AQS內部維護著一個FIFO的同步隊列,當一個線程執行ReetrantLock.lock()方法獲取鎖失敗時,該線程會被封裝成Node節點加入同步隊列等待鎖資源的釋放,期間不斷執行自旋邏輯。當該線程所在節點的前驅節點為隊列頭結點時,當前線程就會開始嘗試對同步狀態標識state進行修改(+1),如果可以修改成功則代表獲取鎖資源成功,然后將自己所在的節點設置為隊頭head節點,表示自己已經持有鎖資源。那么當一個線程調用ReetrantLock.unlock()釋放鎖時,最終會調用Sync內部類中的tryRelease(int releases)方法再次對同步狀態標識state進行修改(-1),成功之后喚醒當前線程所在節點的后繼節點中的線程。

4.2、ReetrantLock中的FairSync公平鎖

在前面我們已經詳細的分析了ReetrantLock中非公平鎖的實現過程,那么我們接下來再去一探ReetrantLock中公平鎖的實現原理。不過在此之前我們先對于需要的公平和非公平的概念有個認知。所謂的公平與非公平是基于線程到來的時間順序為基準來區分的,公平鎖指的是完全遵循FIFO原則的一種模式。也就代表著,在時間順序上來看,公平鎖模式下,先執行獲取鎖邏輯的線程就一定會先持有鎖資源。同理,非公平鎖則反之。下面我們來看一下公平鎖FairSync類中tryAcquire(int acquires)方法的實現。

// ReetrantLock → FairSync → tryAcquire(int acquires)
protected final boolean tryAcquire(int acquires) {
    // 獲取當前線程
    final Thread current = Thread.currentThread();
    // 獲取同步狀態標識值
    int c = getState();
    if (c == 0) { // 如果為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;
}

FairSync類中獲取鎖方法tryAcquire(int acquires)的實現與NonFairSync類中獲取鎖方法nonfairTryAcquire(int acquires)唯一不同的是:公平鎖的實現中,在嘗試修改state之前會先調用hasQueuedPredecessors()判斷AQS內部的同步隊列是否存在節點。如果存在則說明在此之前已經有線程提交了獲取鎖的請求,那么當前線程會被直接封裝成Node節點追加到隊尾等待。而在非公平鎖的tryAcquire(int acquires)實現中,不管隊列中是否已經存在節點,都會先嘗試修改同步狀態標識state獲取鎖,當獲取鎖失敗時才會將當前線程封裝成Node節點加入隊列。但是我們在實際開發過程中,如果不需要考慮業務處理時執行順序的情況下,我們應該優先考慮使用非公平鎖,因為往往在實際應用過程中,非公平鎖的性能會大大超出公平鎖!

4.3、實際開發過程中ReetrantLock與synchronized如何抉擇?

在前面的文章:《徹底理解Java并發編程之Synchronized關鍵字實現原理剖析》中我們曾詳細的談到過Java中的隱式鎖的synchronized的底層實現,我們也曾談到在JDK1.6之后,JVM對于Synchronized關鍵字進行了很大程度上的優化,那么在實際開發過程中我們又該如何在ReetrantLock與synchronized進行選擇呢?synchronized相對來說使用更加方便、語義更清晰,同時JVM也為我們自動優化了。而ReetrantLock則使用起來更加靈活,同時也提供了多樣化的支持,比如超時獲取鎖、可中斷式獲取鎖、等待喚醒機制的多個條件變量(Condition)等。所以在我們需要用到這些功能時我們可以選擇ReetrantLock。但是具體采用哪個還是需要根據業務需求決定。

例如:某個項目在凌晨一點至凌晨五點流量非常巨大,但是其他時間內相對來說訪問頻率并不高,對于這種情況采用哪種鎖更為合適?答案是ReetrantLock。
為什么?因為在前面關于synchronized的文章中我們曾提到過,synchronized的鎖升級/膨脹是不可逆的,幾乎在Java程序運行過程中不會出現鎖降級的情況。那么在這種業務場景下,流量劇增的那段時間會有可能導致synchronized直接膨脹成重量級鎖,而synchronized一旦升級到重量級鎖,那么這把鎖之后的每次獲取鎖都是重量級鎖,這樣會大大的影響程序性能。

4.4、ReetrantLock實現總結

  • 基礎組件:
    • 同步狀態標識:對外顯示鎖資源的占有狀態
    • 同步隊列:存放獲取鎖失敗的線程
    • 等待隊列:用于實現多條件喚醒
    • Node節點:隊列的每個節點,線程封裝體
  • 基礎動作:
    • cas修改同步狀態標識
    • 獲取鎖失敗加入同步隊列阻塞
    • 釋放鎖時喚醒同步隊列第一個節點線程
  • 加鎖動作:
    • 調用tryAcquire()修改標識state,成功返回true執行,失敗加入隊列等待
    • 加入隊列后判斷節點是否為signal狀態,是就直接阻塞掛起當前線程
    • 如果不是則判斷是否為cancel狀態,是則往前遍歷刪除隊列中所有cancel狀態節點
    • 如果節點為0或者propagate狀態則將其修改為signal狀態
    • 阻塞被喚醒后如果為head則獲取鎖,成功返回true,失敗則繼續阻塞
  • 解鎖動作:
    • 調用tryRelease()釋放鎖修改標識state,成功則返回true,失敗返回false
    • 釋放鎖成功后喚醒同步隊列后繼阻塞的線程節點
    • 被喚醒的節點會自動替換當前節點成為head節點

五、多條件等待喚醒機制之神奇的Condition實現原理

在Java并發編程中,每個Java堆中的對象在“出生”的時刻都會“伴生”一個監視器對象,而每個Java對象都會有一組監視器方法:wait()notify()以及notifyAll()。我們可以通過這些方法實現Java多線程之間的協作和通信,也就是等待喚醒機制,如常見的生產者-消費者模型。但是關于Java對象的這組監視器方法我們在使用過程中,是需要配合synchronized關鍵字才能使用,因為實際上Java對象的等待喚醒機制是基于monitor監視器對象實現的。與synchronized關鍵字的等待喚醒機制相比,Condition則更為靈活,因為synchronized的notify()只能隨機喚醒等待鎖的一個線程,而Condition則可以更加細粒度的精準喚醒等待鎖的某個線程。與synchronized的等待喚醒機制不同的是,在monitor監視器模型上,一個對象擁有一個同步隊列和一個等待隊列,而AQS中一個鎖對象擁有一個同步隊列和多個等待隊列。對象監視器Monitor鎖實現原理如下:

Monitor監視器鎖實現原理

5.1、快速認識及上手Condition實戰

Condition是一個接口類,具體實現者為AQS內部的ConditionObject類,Condition中定義方法如下:

public interface Condition {
    /**
    * 調用當前方法會使當前線程處于等待狀態直到被通知(signal)或中斷
    * 當其他線程調用singal()或singalAll()方法時,當前線程將被喚醒
    * 當其他線程調用interrupt()方法中斷當前線程等待狀態
    * await()相當于synchronized等待喚醒機制中的wait()方法
    */
    void await() throws InterruptedException;
    
    /**
    * 作用與await()相同,但是該方法不響應線程中斷操作
    */
    void awaitUninterruptibly();
    
    /**
    * 作用與await()相同,但是該方法支持超時中斷(單位:納秒)
    * 當線程等待時間超出nanosTimeout時則中斷等待狀態
    */
    long awaitNanos(long nanosTimeout) throws InterruptedException;
    
    /**
    * 作用與awaitNanos(long nanosTimeout)相同,但是該方法可以聲明時間單位
    */
    boolean await(long time, TimeUnit unit) throws InterruptedException;
    
    /**
    * 作用與await()相同,在deadline時間內被喚醒返回true,其他情況則返回false
    */
    boolean awaitUntil(Date deadline) throws InterruptedException;
    
    /**
    * 當有線程調用該方法時,喚醒等待隊列中的一個線程節點
    * 并將該線程從等待隊列移動同步隊列阻塞等待鎖資源獲取
    * signal()相當于synchronized等待喚醒機制中的notify()方法
    */
    void signal();
    
    /**
    * 作用與signal()相同,不過該方法的作用是喚醒該等待隊列中的所有線程節點
    * signalAll()相當于synchronized等待喚醒機制中的notifyAll()方法
    */
    void signalAll();
}

如上便是Condition接口中定義的方法,總體可以分為兩類,一類是線程掛起/等待類的await方法,另一類則是線程喚醒類的signal方法,接下來我們運用Condition來實現一個經典的消費者/生產者的小案例簡單了解一下Condition的使用:

public class Bamboo {
    private int bambooCount = 0;
    private boolean flag = false;

    Lock lock = new ReentrantLock();
    Condition producerCondition = lock.newCondition();
    Condition consumerCondition = lock.newCondition();

    public void producerBamboo() {
        lock.lock(); // 獲取鎖資源
        try {
            while (flag) { // 如果有竹子
                try {
                    producerCondition.await(); // 掛起生產竹子的線程
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            bambooCount++; // 竹子數量+1
            System.out.println(Thread.currentThread().getName() + "....生產竹子,目前竹子數量:" + bambooCount);
            flag = true; // 竹子余量狀態改為true
            consumerCondition.signal(); // 生產好竹子之后,喚醒消費竹子的線程
        } finally {
            lock.unlock(); // 釋放鎖資源
        }
    }

    public void consumerBamboo() {
        lock.lock(); // 獲取鎖資源
        try {
            while (!flag) { // 如果沒有竹子
                try {
                    consumerCondition.await(); // 掛起消費竹子的線程
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            bambooCount--; // 竹子數量-1
            System.out.println(Thread.currentThread().getName() + "....消費竹子,目前竹子數量:" + bambooCount);
            flag = false; // 竹子余量狀態改為false
            producerCondition.signal(); // 消費完成竹子之后,喚醒生產竹子的線程
        } finally {
            lock.unlock(); // 釋放鎖資源
        }
    }
}
/**------------------分割線--------------------**/
// 測試類
public class ConditionDemo {
    public static void main(String[] args){
        Bamboo b = new Bamboo();
        Producer producer = new Producer(b);
        Consumer consumer = new Consumer(b);

        // 生產者線程組
        Thread t1 = new Thread(producer,"生產者-t1");
        Thread t2 = new Thread(producer,"生產者-t2");
        Thread t3 = new Thread(producer,"生產者-t3");

        // 消費者線程組
        Thread t4 = new Thread(consumer,"消費者-t4");
        Thread t5 = new Thread(consumer,"消費者-t5");
        Thread t6 = new Thread(consumer,"消費者-t6");

        t1.start();
        t2.start();
        t3.start();
        t4.start();
        t5.start();
        t6.start();
    }
}
// 生產者
class Producer implements Runnable{
    private Bamboo bamboo;

    public Producer(Bamboo bamboo) {
        this.bamboo = bamboo;
    }
    @Override
    public void run() {
        for (;;){
            bamboo.producerBamboo();
        }
    }
}

// 生產者
class Consumer implements Runnable{
    private Bamboo bamboo;

    public Consumer(Bamboo bamboo) {
        this.bamboo = bamboo;
    }
    @Override
    public void run() {
        for (;;){
            bamboo.consumerBamboo();
        }
    }
}

如上代碼中運用一個生產/消費竹子的案例簡單的使用了一下Condition。在該案例中存在六條線程,t1,t2,t3為生產者線程組,t4,t5,t6為消費者線程組,六條線程同時執行,需要保證生產線程組先生產竹子后消費者線程組才能消費竹子,否則消費者線程組的線程只能等待直至生產者線程組生產出竹子為止,不能出現重復消費的情況。在Bamboo類中定義了兩個方法:producerBamboo()以及consumerBamboo()用于生產和消費竹子。并且同時定義了一個全局的ReetrantLock鎖,用于保證兩組線程在同時執行過程中不出現線程安全問題。而因為需要保證生產/消費的前后順序,所以基于lock鎖對象創建了兩個等待條件:producerCondition、consumerCondition,前者控制生產線程組在竹子數量不為零時,生產線程等待,后者則控制消費者線程組。這里同時定義了一個flag標志對外展示竹子的余量情況,為false則代表沒有竹子,需先生產竹子,生產完成后喚醒消費者線程,為true時則反之。

在如上案例中對比synchronized的等待/喚醒機制來說,優勢在于可以創建兩個等待條件producerCondition、consumerCondition,因為存在兩個等待隊列,所以可以精準的控制生產者線程組和消費者線程組。而如果使用synchronized的wait()/notify()來實現如上案例則可能出現消費線程在消費完成竹子之后喚醒線程時喚醒的還是消費線程這種情況,因為在Monitor對象中只存在一個等待隊列,如果在synchronized想避免出現這種問題出現則只能使用notifyAll()喚醒等待隊列中的所有線程。但是因為需要喚醒等待隊列中的所有線程,所以性能方面會比Condition慢上許多。

5.2、Condition實現原理分析

在前面我們提到過,Condition只是一個接口,具體的落地實現者為AQS內部的ConditionObject類,在本文最開始分析AQS時我們也曾提到,在AQS內部存在兩種隊列:同步隊列以及等待隊列,等待隊列則是基于Condition而言的。同步隊列與等待隊列中的節點類型都是AQS內部的Node構成的,只不過等待隊列中的Node節點的waitStatus為CONDITION狀態。在ConditionObject類中存在兩個節點:firstWaiter、lastWaiter用于存儲等待隊列中的隊首節點以及隊尾節點,每個節點使用Node.nextWaiter存儲下一個節點的引用,因此等待隊列是一個單向隊列。所以AQS同步器的總體結構如下:

AQS整體結構

如上圖,與同步隊列不同的是:每個Condition都對應一個等待隊列,如果在一個ReetrantLock鎖上創建多個Condition,也就相當于會存在多個等待隊列。同時,雖然同步隊列與等待隊列中的節點都是由Node類構成的,但是同步隊列中的Node節點是存在pred前驅節點以及next后繼節點引用的雙向鏈表類型,而等待隊列中的每個節點則只使用nextWaiter存儲后繼節點引用的單向鏈表類型。但是與同步隊列一致,等待隊列也是一種FIFO的隊列,隊列每個節點都會存儲Condition對象上等待的線程信息。當一個線程調用await掛起類的方法時,該線程首先會釋放鎖,同時構建一個Node節點封裝線程的相關信息,并將其加入等待隊列,直到被喚醒、中斷或者超時才會從隊列中移除。下面我們從源碼角度探究Condition等待/喚醒機制的原理:

public final void await() throws InterruptedException {
    // 判斷線程是否出現中斷信號
    if (Thread.interrupted())
       // 響應中斷則直接拋出異常中斷線程執行
      throw new InterruptedException();
    // 封裝線程信息構建新的節點加入等待隊列并返回
    Node node = addConditionWaiter();
    // 釋放當前線程持有的鎖鎖資源,不管當前線程重入多少次,全部置0
    int savedState = fullyRelease(node);
    int interruptMode = 0;
    // 判斷節點是否在同步隊列(SyncQueue)中,即是否被喚醒
    while (!isOnSyncQueue(node)) {
      // 如果不需要喚醒,則在JVM級別掛起當前線程
      LockSupport.park(this);
      // 判斷是否被中斷喚醒,如果是退出循環
      if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
          break;
    }
    // 被喚醒后執行自旋操作嘗試獲取鎖,同時判斷線程是否被中斷
    if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
      interruptMode = REINTERRUPT;
    // 取消后進行清理
    if (node.nextWaiter != null) 
      // 清理等待隊列中不為CONDITION狀態的節點
      unlinkCancelledWaiters();
    if (interruptMode != 0)
      reportInterruptAfterWait(interruptMode);
}

// 構建節點封裝線程信息入隊方法
private Node addConditionWaiter() {
    Node t = lastWaiter;
    // 判斷節點狀態是否為結束狀態,如果是則移除
    if (t != null && t.waitStatus != Node.CONDITION) {
        unlinkCancelledWaiters();
        t = lastWaiter;
    }
    // 構建新的節點封裝當前線程相關信息,節點狀態為CONDITION等待狀態
    Node node = new Node(Thread.currentThread(), Node.CONDITION);
    // 將節點加入隊列
    if (t == null)
        firstWaiter = node;
    else
        t.nextWaiter = node;
    lastWaiter = node;
    return node;
}

從如上代碼觀察中,不難發現,await()主要做了四件事:

  • 一、調用addConditionWaiter()方法構建新的節點封裝線程信息并將其加入等待隊列
  • 二、調用fullyRelease(node)釋放鎖資源(不管此時持有鎖的線程重入多少次都一律將state置0),同時喚醒同步隊列中后繼節點的線程。
  • 三、調用isOnSyncQueue(node)判斷節點是否存在同步隊列中,在這里是一個自旋操作,如果同步隊列中不存在當前節點則直接在JVM級別掛起當前線程
  • 四、當前節點線程被喚醒后,即節點從等待隊列轉入同步隊列時,則調用acquireQueued(node, savedState)方法執行自旋操作嘗試重新獲取鎖資源

至此,整個await()方法結束,整個線程從調用await()方法→構建節點入列→釋放鎖資源喚醒同步隊列后繼節點→JVM級別掛起線程→喚醒競爭鎖資源流程完結。其他await()等待類方法原理類似則不再贅述,下面我們再來看看singal()喚醒方法:

public final void signal() {
     // 判斷當前線程是否持有獨占鎖資源,如果未持有則直接拋出異常
    if (!isHeldExclusively())
          throw new IllegalMonitorStateException();
      Node first = firstWaiter;
      // 喚醒等待隊列第一個節點的線程
      if (first != null)
          doSignal(first);
}

在這里,singal()喚醒方法一共做了兩件事:

  • 一、判斷當前線程是否持有獨占鎖資源,如果調用喚醒方法的線程未持有鎖資源則直接拋出異常(共享模式下沒有等待隊列,所以無法使用Condition)
  • 二、喚醒等待隊列中的第一個節點的線程,即調用doSignal(first)方法

下面我們來看看doSignal(first)方法的實現:

private void doSignal(Node first) {
    do {
        // 移除等待隊列中的第一個節點,如果nextWaiter為空
        // 則代表著等待隊列中不存在其他節點,那么將尾節點也置空
        if ( (firstWaiter = first.nextWaiter) == null)
            lastWaiter = null;
        first.nextWaiter = null;
    // 如果被通知上個喚醒的節點沒有進入同步隊列(可能出現被中斷的情況),
    // 等待隊列中還存在其他節點則繼續循環喚醒后繼節點的線程
    } while (!transferForSignal(first) &&
             (first = firstWaiter) != null);
}

// transferForSignal()方法
final boolean transferForSignal(Node node) {
    /*
     * 嘗試修改被喚醒節點的waitStatus為0即初始化狀態
     *      如果設置失敗則代表著當前節點的狀態不為CONDITION等待狀態,
     *      而是結束狀態了則返回false返回doSignal()繼續喚醒后繼節點
     *  為什么說設置失敗則代表著節點不為CONDITION等待狀態?
     *      因為可以執行到此處的線程必定是持有獨占鎖資源的,
     *      而此處使用的是cas機制修改waitStatus,失敗的原因只有一種:
     *          預期值waitStatus不等于CONDITION
     */
    if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
        return false;
    // 快速追加到同步隊列尾部,同時返回前驅節點p
    Node p = enq(node);
    // 判斷前驅節點狀態是否為結束狀態或者在設置前驅節點狀態為SIGNAL失敗時,
    // 喚醒被通知節點內的線程
    int ws = p.waitStatus;
    if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
        // 喚醒node節點內的線程
        LockSupport.unpark(node.thread);
    return true;
}

在如上代碼中,可以通過我的注釋發現,doSignal()也只做了三件事:

  • 一、將被喚醒的第一個節點從等待隊列中移除,然后再維護等待隊列中firstWaiter和lastWaiter的指向節點引用
  • 二、將等待隊列中移除的節點追加到同步隊列尾部,如果同步隊列追加失敗或者等待隊列中還存在其他節點的話,則繼續循環喚醒其他節點的線程
  • 三、加入同步隊列成功后,如果前驅節點狀態已經為結束狀態或者在設置前驅節點狀態為SIGNAL失敗時,直接通過LockSupport.unpark()喚醒節點內的線程

至此,Signal()方法邏輯結束,不過需要注意的是:我們在理解Condition的等待/喚醒原理的時候需要將await()/signal()方法結合起來理解。在signal()邏輯完成后,被喚醒的線程則會從前面的await()方法的自旋中退出,因為當前線程所在的節點已經被移入同步隊列,所以while (!isOnSyncQueue(node))條件不成立,循環自然則終止,進而被喚醒的線程會調用acquireQueued()開始嘗試獲取鎖資源。

六、Condition接口與Monitor對象等待/喚醒機制的區別

最后我們來簡單的對比一下ReetrantLock的Condition多條件等待/喚醒機制與與Synchronized的Monitor對象鎖等待/喚醒機制之間的區別:

對比項 Monitor Condition
前置條件 需持有對象鎖 需持有獨占鎖且創建Condition對象
調用方式 Object.wait() condition.await類方法都可
隊列數量 一個 多個
等待時釋放鎖資源 支持 支持
線程中斷 不支持 支持
超時中斷 不支持 支持
超時等待 支持 支持
精準喚醒線程 不支持 支持
喚醒全部線程 支持 支持

至此AQS獨占模式的實現原理分析則告一段落,下篇文章我們在進一步探究共享模式的具體實現~
下篇:(六)手撕并發編程之基于Semaphore與CountDownLatch分析AQS共享模式實現

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,156評論 6 529
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 97,866評論 3 413
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 174,880評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,398評論 1 308
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,202評論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,743評論 1 320
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,822評論 3 438
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 41,962評論 0 285
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,476評論 1 331
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,444評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,579評論 1 365
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,129評論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,840評論 3 344
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,231評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,487評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,177評論 3 388
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,568評論 2 370

推薦閱讀更多精彩內容