(五)深入剖析并發(fā)之AQS獨(dú)占鎖&重入鎖(ReetrantLock)及Condition實(shí)現(xiàn)原理

引言

在我們前面的文章《深入理解Java并發(fā)編程之無鎖CAS機(jī)制》中我們?cè)岬降腃AS機(jī)制如果說是整個(gè)Java并發(fā)編程基礎(chǔ)的話,那么本章跟大家所講述的AQS則是整個(gè)Java JUC的核心。不過在學(xué)習(xí)AQS之前需要對(duì)于CAS機(jī)制有一定的知識(shí)儲(chǔ)備,因?yàn)镃AS在ReetrantLock及AQS中的實(shí)現(xiàn)隨處可見。

一、JUC中的Lock鎖接口

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

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

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

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

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

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

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


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

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

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

二、Lock接口的實(shí)現(xiàn)者:ReetrantLock重入鎖

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

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);
        // 執(zhí)行結(jié)果:20000
    }
}

上面的這個(gè)例子很簡(jiǎn)單,t1,t2兩個(gè)線程同時(shí)對(duì)共享資源count進(jìn)行++的非原子性操作,我們?cè)谶@里使用ReentrantLock鎖解決存在的線程安全問題。同時(shí)我們?cè)谏鲜龃a中,獲取了兩次鎖資源,因?yàn)镽eentrantLock支持鎖重入,所以此時(shí)獲取兩次鎖是沒有問題的,不過在finally中執(zhí)行釋放鎖資源時(shí)需要注意:也應(yīng)該執(zhí)行兩次unlock釋放鎖的操作。從上述案例中分析我們可以發(fā)現(xiàn),其實(shí)ReentrantLock的用法相對(duì)來說比較簡(jiǎn)單,我們接下來也可以分析一下ReentrantLock所提供的一些方法以便于更加全面的認(rèn)識(shí)它。如下:

// 查詢當(dāng)前線程調(diào)用lock()的次數(shù)
int getHoldCount() 

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

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

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

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

// 返回調(diào)用當(dāng)前鎖資源Condition對(duì)象await方法后未執(zhí)行signal()方法的線程估計(jì)數(shù)
int getWaitQueueLength(Condition condition);

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

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

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

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

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

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

通過觀察我們不難得知,ReentrantLock作為L(zhǎng)ock接口的實(shí)現(xiàn)者,除開在實(shí)現(xiàn)了Lock接口定義的方法外,ReentrantLock也拓展了一些其他方法。我們可以通過一個(gè)簡(jiǎn)單的案例來熟悉一下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的簡(jiǎn)單使用案例
    @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 {
            //獲取當(dāng)前線程調(diào)用lock()方法的次數(shù)
            System.out.println("線程:" + Thread.currentThread().getName() + "\t調(diào)用lock()次數(shù):" + lock.getHoldCount());
            // 判斷當(dāng)前鎖是否為公平鎖
            System.out.println("當(dāng)前鎖資源類型是否為公平鎖?" + lock.isFair());
            // 獲取等待獲取當(dāng)前鎖資源的估計(jì)線程數(shù)
            System.out.println("目前有:" + lock.getQueueLength() + "個(gè)線程正在等待獲取鎖資源!");
            // 指定線程是否在等待獲取當(dāng)前鎖資源
            System.out.println("當(dāng)前線程是否在等待獲取當(dāng)前鎖資源?" + lock.hasQueuedThread(Thread.currentThread()));
            // 判斷當(dāng)前鎖資源是否有線程在等待獲取
            System.out.println("當(dāng)前鎖資源是否存在線程等待獲取?" + lock.hasQueuedThreads());
            // 判斷當(dāng)前線程是否持有當(dāng)前鎖資源
            System.out.println("當(dāng)前線程是否持有當(dāng)前鎖資源?" + lock.isHeldByCurrentThread());
            // 判斷當(dāng)前鎖資源是否被線程持有
            System.out.println("當(dāng)前鎖資源是否被線程占用?" + 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); // 執(zhí)行結(jié)果:20000
        /**
         * 執(zhí)行結(jié)果:
         *   線程:main    調(diào)用lock()次數(shù):1
         *   當(dāng)前鎖資源類型是否為公平鎖?false
         *   目前有:0個(gè)線程正在等待獲取鎖資源!
         *   當(dāng)前線程是否在等待獲取當(dāng)前鎖資源?false
         *   當(dāng)前鎖資源是否存在線程等待獲???false
         *   當(dāng)前線程是否持有當(dāng)前鎖資源?true
         *   當(dāng)前鎖資源是否被線程占用?true
         */
        task.reentrantLockApiTest();
    }
}

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

三、JUC并發(fā)包內(nèi)核:并發(fā)基礎(chǔ)組件AQS

AQS全稱為AbstractQueuedSynchronizer(抽象的隊(duì)列同步器),Java并發(fā)包中的核心基礎(chǔ)組件,它是用來構(gòu)建信號(hào)量、鎖、門閥等其他同步組件的基礎(chǔ)框架。

AQS工作原理簡(jiǎn)述

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

public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer{
// 指向同步隊(duì)列的頭部
private transient volatile Node head;
// 指向同步隊(duì)列的尾部
private transient volatile Node tail;
// 同步狀態(tài)標(biāo)識(shí)
private volatile int state;
// 省略......
}

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

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

    // 等待狀態(tài),存在CANCELLED、SIGNAL、CONDITION、PROPAGATE四種
    volatile int waitStatus;

    // 同步隊(duì)列中前驅(qū)結(jié)點(diǎn)
    volatile Node prev;

    // 同步隊(duì)列中后繼結(jié)點(diǎn)
    volatile Node next;

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

    // 等待隊(duì)列中的后繼結(jié)點(diǎn)(與Condition有關(guān),稍后會(huì)分析)
    Node nextWaiter;

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

在其中SHARED和EXCLUSIVE兩個(gè)全局常量分別代表著共享模式和獨(dú)占模式,共享模式即允許多個(gè)線程同時(shí)對(duì)一個(gè)鎖資源進(jìn)行操作,例如:信號(hào)量Semaphore、讀鎖ReadLock等采用的就是基于AQS的共享模式實(shí)現(xiàn)的。而獨(dú)占模式則代表著在同一時(shí)刻只運(yùn)行一個(gè)線程對(duì)鎖資源進(jìn)行操作,如ReentranLock等組件的實(shí)現(xiàn)都是基于AQS的獨(dú)占模式實(shí)現(xiàn)。全局變量waitStatus則代表著當(dāng)前被封裝成Node節(jié)點(diǎn)的線程的狀態(tài),一共存在五種情況:

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

Node節(jié)點(diǎn)結(jié)構(gòu)

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

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

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

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

到此我們對(duì)于AQS這個(gè)并發(fā)核心組件的原理大致有了一定了解,接下來我們會(huì)帶著大家基于ReetrantLock進(jìn)一步分析AQS的具體實(shí)現(xiàn)過程。

四、基于ReetrantLock分析AQS獨(dú)占模式實(shí)現(xiàn)過程及原理

4.1、ReetrantLock中的NonfairSync非公平鎖

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

// 構(gòu)造函數(shù):默認(rèn)創(chuàng)建的鎖屬于非公平鎖(NonfairSync)類型
public ReentrantLock() {
    sync = new NonfairSync();
}
// 構(gòu)造函數(shù):根據(jù)傳入?yún)?shù)創(chuàng)建鎖類型(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() {
        // 執(zhí)行CAS操作,修改同步狀態(tài)標(biāo)識(shí)獲取鎖資源
        // 因?yàn)榇嬖诙鄺l線程同時(shí)修改的可能,所以需要用CAS操作保證原子性
        if (compareAndSetState(0, 1))
            // 成功則將獨(dú)占鎖線程設(shè)置為當(dāng)前線程  
            setExclusiveOwnerThread(Thread.currentThread());
        else acquire(1); // 否則再次請(qǐng)求同步狀態(tài)
    }
}

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

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

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

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

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

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

  • 一、嘗試重新修改同步標(biāo)識(shí)獲取鎖資源(因?yàn)榭赡艽嬖谏蟼€(gè)獲取鎖的線程在當(dāng)前線程上次獲取鎖失敗到目前這段時(shí)間之前釋放了鎖),成功則將獨(dú)占鎖線程設(shè)置為當(dāng)前獲取同步狀態(tài)的線程,最后返回ture。
  • 二、判斷當(dāng)前線程current是否為獨(dú)占鎖線程OwnerThread,如果是則代表著當(dāng)前線程已經(jīng)獲取過鎖資源還未釋放,屬于鎖重入,那么對(duì)state進(jìn)行自增1,返回true。
  • 如果當(dāng)前線程前面兩個(gè)判斷都不滿足,則返回false,也就代表著nonfairTryAcquire(acquires)執(zhí)行結(jié)束。

不過在這個(gè)方法中值得注意的是,nonfairTryAcquire(acquires)方法中修改state同步標(biāo)識(shí)時(shí)使用的是cas操作保證線程安全,因此只要任意一個(gè)線程調(diào)用nonfairTryAcquire(acquires)方法并設(shè)置成功即可獲取鎖,不管該線程是新到來的還是已在同步隊(duì)列的線程,畢竟這是非公平鎖,并不保證同步隊(duì)列中的線程一定比新到來線程請(qǐng)求(可能是head結(jié)點(diǎn)剛釋放同步狀態(tài)然后新到來的線程恰好獲取到同步狀態(tài))先獲取到鎖,這點(diǎn)跟后面還會(huì)分析的公平鎖不同。那么我們?cè)俅位氐街癗onfairSync類中的lock()方法中調(diào)用的acquire(1)方法:

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

在這里,如果tryAcquire(arg)執(zhí)行后能夠成功獲取鎖返回true,這個(gè)if自然不用繼續(xù)往下執(zhí)行,這是最理想的狀態(tài)。但是如果當(dāng)tryAcquire(arg)返回false時(shí),則會(huì)繼續(xù)執(zhí)行addWaiter(Node.EXCLUSIVE)封裝線程入列操作(因?yàn)镽eetrantLock屬于獨(dú)占式鎖,所以Node節(jié)點(diǎn)類型屬于Node.EXCLUSIVE)。addWaiter方法代碼如下:

private Node addWaiter(Node mode) {
    // 將請(qǐng)求同步狀態(tài)失敗的線程封裝成Node節(jié)點(diǎn)
    Node node = new Node(Thread.currentThread(), mode);

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

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

private Node enq(final Node node) {
    // 死循環(huán)
    for (;;) {
         Node t = tail;
         // 如果隊(duì)列為null,即沒有頭結(jié)點(diǎn)
         if (t == null) { // Must initialize
             // 創(chuàng)建并使用CAS設(shè)置頭結(jié)點(diǎn)
             if (compareAndSetHead(new Node()))
                 tail = head;
         } else { // 隊(duì)尾添加新結(jié)點(diǎn)
             node.prev = t;
             if (compareAndSetTail(t, node)) {
                 t.next = node;
                 return t;
             }
         }
     }
}

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

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

添加到同步隊(duì)列的節(jié)點(diǎn)都會(huì)進(jìn)入一個(gè)自旋過程,每個(gè)節(jié)點(diǎn)都在觀察時(shí)機(jī)等待條件滿足時(shí),開始獲取同步狀態(tài),然后從同步隊(duì)列中退出并結(jié)束自旋,回到之前的acquire()方法,自旋過程是在acquireQueued(addWaiter(Node.EXCLUSIVE),arg))方法中執(zhí)行的,代碼如下:

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

當(dāng)前節(jié)點(diǎn)中的線程在死循環(huán)(自旋)執(zhí)行過程中,當(dāng)節(jié)點(diǎn)的前驅(qū)節(jié)點(diǎn)為頭節(jié)點(diǎn)時(shí)開始嘗試獲取同步狀態(tài)(符合FIFO原則)。head節(jié)點(diǎn)是當(dāng)前占有同步狀態(tài)標(biāo)識(shí)的線程節(jié)點(diǎn),只有當(dāng)head節(jié)點(diǎn)釋放同步狀態(tài)喚醒后繼節(jié)點(diǎn)時(shí),后繼節(jié)點(diǎn)才可能獲取同步狀態(tài),所以這也是為什么說:只有當(dāng)節(jié)點(diǎn)的前驅(qū)節(jié)點(diǎn)為頭節(jié)點(diǎn)時(shí)才開始嘗試獲取同步狀態(tài)的原因,在此之外的其他時(shí)候?qū)⒈粧炱?。如果?dāng)前節(jié)點(diǎn)已經(jīng)開始嘗試獲取同步狀態(tài),進(jìn)入if后則會(huì)執(zhí)行setHead()方法將當(dāng)前線程設(shè)置為head節(jié)點(diǎn),如下:

// 將傳遞的節(jié)點(diǎn)設(shè)置為同步隊(duì)列的頭節(jié)點(diǎn)
private void setHead(Node node) {
    head = node;
    // 清空當(dāng)前節(jié)點(diǎn)存儲(chǔ)的數(shù)據(jù)信息
    node.thread = null;
    node.prev = null;
}

node節(jié)點(diǎn)被設(shè)置為head頭節(jié)點(diǎn)后,當(dāng)前節(jié)點(diǎn)存儲(chǔ)的線程以及前驅(qū)節(jié)點(diǎn)信息將會(huì)清空,因?yàn)楫?dāng)前線程已經(jīng)成功獲取到了鎖資源,沒有必要再存儲(chǔ)線程信息,同時(shí)因?yàn)楫?dāng)前節(jié)點(diǎn)已經(jīng)成為了頭節(jié)點(diǎn),不存在前驅(qū)節(jié)點(diǎn)了,所以也會(huì)被清空信息。head節(jié)點(diǎn)只保留指向后繼節(jié)點(diǎn)的信息方便當(dāng)前節(jié)點(diǎn)釋放鎖資源時(shí)喚醒后繼線程。如上則是節(jié)點(diǎn)的前驅(qū)節(jié)點(diǎn)為頭節(jié)點(diǎn)時(shí)會(huì)執(zhí)行的邏輯,如果節(jié)點(diǎn)的前驅(qū)節(jié)點(diǎn)并不是head則會(huì)執(zhí)行if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) interrupted = true;邏輯,代碼如下:

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    // 獲取當(dāng)前節(jié)點(diǎn)的等待狀態(tài)
    int ws = pred.waitStatus;
    // 如果為等待喚醒(SIGNAL)狀態(tài)則返回true
    if (ws == Node.SIGNAL)
        return true;
    // 如果當(dāng)前節(jié)點(diǎn)等待狀態(tài)大于0則說明是結(jié)束狀態(tài),
    // 遍歷前驅(qū)節(jié)點(diǎn)直到找到?jīng)]有結(jié)束狀態(tài)的節(jié)點(diǎn)
    if (ws > 0) {
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else {
        // 如果當(dāng)前節(jié)點(diǎn)等待狀態(tài)小于0又不是SIGNAL狀態(tài),
        // 則將其設(shè)置為SIGNAL狀態(tài),代表該節(jié)點(diǎn)的線程正在等待喚醒
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}

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

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

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

tryAcquire(arg)執(zhí)行過程

AQS之圖解獨(dú)占式獲取鎖過程

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

在前面已經(jīng)帶著大家詳細(xì)的談到了ReetrantLock.lock()方法的具體實(shí)現(xiàn)原理了,那么我們?cè)陂_發(fā)過程中,有時(shí)還會(huì)用到可中斷的獲取方式加鎖,例如調(diào)用ReetrantLock的lockInterruptibly()、tryLock(),那么這些方法最終底層都會(huì)間接的調(diào)用到doAcquireInterruptibly()方法。如下:

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

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

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

在可中斷式獲取鎖資源的方式中,當(dāng)檢測(cè)到線程的中斷操作后,直接拋出異常,從而中斷線程的同步狀態(tài)請(qǐng)求,移除同步隊(duì)列。

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

一般而言,我們?cè)谑褂肦eetrantLock這類顯式鎖時(shí),獲取鎖之后也需要我們手動(dòng)釋放鎖資源。在ReetrantLock中當(dāng)你調(diào)用了lock()獲取鎖資源之后,也需要我們手動(dòng)調(diào)用unlock()釋放鎖。unlock()釋放鎖的代碼如下:

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

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

// ReentrantLock → Sync → tryRelease(int releases)方法
protected final boolean tryRelease(int releases) {
  // 對(duì)于同步狀態(tài)進(jìn)行修改:獲取鎖是+,釋放鎖則為-
  int c = getState() - releases;
  // 如果當(dāng)前釋放鎖的線程不為持有鎖的線程則拋出異常
  if (Thread.currentThread() != getExclusiveOwnerThread())
      throw new IllegalMonitorStateException();
  boolean free = false;
  // 判斷狀態(tài)是否為0,如果是則說明已釋放同步狀態(tài)
  if (c == 0) {
      free = true;
      // 設(shè)置Owner為null
      setExclusiveOwnerThread(null);
  }
  // 設(shè)置更新同步狀態(tài)
  setState(c);
  return free;
}

釋放鎖的邏輯相對(duì)與獲取鎖的邏輯來說要簡(jiǎn)單許多,unlock()方法最終是調(diào)用tryRelease(int releases)釋放鎖的,而tryRelease(int releases)則是ReetrantLock實(shí)現(xiàn)的方法,因?yàn)樵贏QS中沒有提供具體實(shí)現(xiàn),而是交由了子類自己實(shí)現(xiàn)具體的邏輯。釋放鎖資源后會(huì)使用unparkSuccessor(h)喚醒后繼節(jié)點(diǎn)的線程。unparkSuccessor(h)的代碼如下:

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

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

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

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

4.2、ReetrantLock中的FairSync公平鎖

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

// ReetrantLock → FairSync → tryAcquire(int acquires)
protected final boolean tryAcquire(int acquires) {
    // 獲取當(dāng)前線程
    final Thread current = Thread.currentThread();
    // 獲取同步狀態(tài)標(biāo)識(shí)值
    int c = getState();
    if (c == 0) { // 如果為0代表目前沒有線程持有鎖資源
    // 在公平鎖實(shí)現(xiàn)中這里先判斷同步隊(duì)列是否存在節(jié)點(diǎn)
        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)的實(shí)現(xiàn)與NonFairSync類中獲取鎖方法nonfairTryAcquire(int acquires)唯一不同的是:公平鎖的實(shí)現(xiàn)中,在嘗試修改state之前會(huì)先調(diào)用hasQueuedPredecessors()判斷AQS內(nèi)部的同步隊(duì)列是否存在節(jié)點(diǎn)。如果存在則說明在此之前已經(jīng)有線程提交了獲取鎖的請(qǐng)求,那么當(dāng)前線程會(huì)被直接封裝成Node節(jié)點(diǎn)追加到隊(duì)尾等待。而在非公平鎖的tryAcquire(int acquires)實(shí)現(xiàn)中,不管隊(duì)列中是否已經(jīng)存在節(jié)點(diǎn),都會(huì)先嘗試修改同步狀態(tài)標(biāo)識(shí)state獲取鎖,當(dāng)獲取鎖失敗時(shí)才會(huì)將當(dāng)前線程封裝成Node節(jié)點(diǎn)加入隊(duì)列。但是我們?cè)趯?shí)際開發(fā)過程中,如果不需要考慮業(yè)務(wù)處理時(shí)執(zhí)行順序的情況下,我們應(yīng)該優(yōu)先考慮使用非公平鎖,因?yàn)橥趯?shí)際應(yīng)用過程中,非公平鎖的性能會(huì)大大超出公平鎖!

4.3、實(shí)際開發(fā)過程中ReetrantLock與synchronized如何抉擇?

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

例如:某個(gè)項(xiàng)目在凌晨一點(diǎn)至凌晨五點(diǎn)流量非常巨大,但是其他時(shí)間內(nèi)相對(duì)來說訪問頻率并不高,對(duì)于這種情況采用哪種鎖更為合適?答案是ReetrantLock。
為什么?因?yàn)樵谇懊骊P(guān)于synchronized的文章中我們?cè)岬竭^,synchronized的鎖升級(jí)/膨脹是不可逆的,幾乎在Java程序運(yùn)行過程中不會(huì)出現(xiàn)鎖降級(jí)的情況。那么在這種業(yè)務(wù)場(chǎng)景下,流量劇增的那段時(shí)間會(huì)有可能導(dǎo)致synchronized直接膨脹成重量級(jí)鎖,而synchronized一旦升級(jí)到重量級(jí)鎖,那么這把鎖之后的每次獲取鎖都是重量級(jí)鎖,這樣會(huì)大大的影響程序性能。

4.4、ReetrantLock實(shí)現(xiàn)總結(jié)

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

五、多條件等待喚醒機(jī)制之神奇的Condition實(shí)現(xiàn)原理

在Java并發(fā)編程中,每個(gè)Java堆中的對(duì)象在“出生”的時(shí)刻都會(huì)“伴生”一個(gè)監(jiān)視器對(duì)象,而每個(gè)Java對(duì)象都會(huì)有一組監(jiān)視器方法:wait()、notify()以及notifyAll()。我們可以通過這些方法實(shí)現(xiàn)Java多線程之間的協(xié)作和通信,也就是等待喚醒機(jī)制,如常見的生產(chǎn)者-消費(fèi)者模型。但是關(guān)于Java對(duì)象的這組監(jiān)視器方法我們?cè)谑褂眠^程中,是需要配合synchronized關(guān)鍵字才能使用,因?yàn)閷?shí)際上Java對(duì)象的等待喚醒機(jī)制是基于monitor監(jiān)視器對(duì)象實(shí)現(xiàn)的。與synchronized關(guān)鍵字的等待喚醒機(jī)制相比,Condition則更為靈活,因?yàn)閟ynchronized的notify()只能隨機(jī)喚醒等待鎖的一個(gè)線程,而Condition則可以更加細(xì)粒度的精準(zhǔn)喚醒等待鎖的某個(gè)線程。與synchronized的等待喚醒機(jī)制不同的是,在monitor監(jiān)視器模型上,一個(gè)對(duì)象擁有一個(gè)同步隊(duì)列和一個(gè)等待隊(duì)列,而AQS中一個(gè)鎖對(duì)象擁有一個(gè)同步隊(duì)列和多個(gè)等待隊(duì)列。對(duì)象監(jiān)視器Monitor鎖實(shí)現(xiàn)原理如下:

Monitor監(jiān)視器鎖實(shí)現(xiàn)原理

5.1、快速認(rèn)識(shí)及上手Condition實(shí)戰(zhàn)

Condition是一個(gè)接口類,具體實(shí)現(xiàn)者為AQS內(nèi)部的ConditionObject類,Condition中定義方法如下:

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

如上便是Condition接口中定義的方法,總體可以分為兩類,一類是線程掛起/等待類的await方法,另一類則是線程喚醒類的signal方法,接下來我們運(yùn)用Condition來實(shí)現(xiàn)一個(gè)經(jīng)典的消費(fèi)者/生產(chǎn)者的小案例簡(jiǎn)單了解一下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(); // 掛起生產(chǎn)竹子的線程
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            bambooCount++; // 竹子數(shù)量+1
            System.out.println(Thread.currentThread().getName() + "....生產(chǎn)竹子,目前竹子數(shù)量:" + bambooCount);
            flag = true; // 竹子余量狀態(tài)改為true
            consumerCondition.signal(); // 生產(chǎn)好竹子之后,喚醒消費(fèi)竹子的線程
        } finally {
            lock.unlock(); // 釋放鎖資源
        }
    }

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

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

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

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

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

// 生產(chǎn)者
class Consumer implements Runnable{
    private Bamboo bamboo;

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

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

在如上案例中對(duì)比synchronized的等待/喚醒機(jī)制來說,優(yōu)勢(shì)在于可以創(chuàng)建兩個(gè)等待條件producerCondition、consumerCondition,因?yàn)榇嬖趦蓚€(gè)等待隊(duì)列,所以可以精準(zhǔn)的控制生產(chǎn)者線程組和消費(fèi)者線程組。而如果使用synchronized的wait()/notify()來實(shí)現(xiàn)如上案例則可能出現(xiàn)消費(fèi)線程在消費(fèi)完成竹子之后喚醒線程時(shí)喚醒的還是消費(fèi)線程這種情況,因?yàn)樵贛onitor對(duì)象中只存在一個(gè)等待隊(duì)列,如果在synchronized想避免出現(xiàn)這種問題出現(xiàn)則只能使用notifyAll()喚醒等待隊(duì)列中的所有線程。但是因?yàn)樾枰獑拘训却?duì)列中的所有線程,所以性能方面會(huì)比Condition慢上許多。

5.2、Condition實(shí)現(xiàn)原理分析

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

AQS整體結(jié)構(gòu)

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

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

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

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

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

至此,整個(gè)await()方法結(jié)束,整個(gè)線程從調(diào)用await()方法→構(gòu)建節(jié)點(diǎn)入列→釋放鎖資源喚醒同步隊(duì)列后繼節(jié)點(diǎn)→JVM級(jí)別掛起線程→喚醒競(jìng)爭(zhēng)鎖資源流程完結(jié)。其他await()等待類方法原理類似則不再贅述,下面我們?cè)賮砜纯磗ingal()喚醒方法:

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

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

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

下面我們來看看doSignal(first)方法的實(shí)現(xiàn):

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

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

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

  • 一、將被喚醒的第一個(gè)節(jié)點(diǎn)從等待隊(duì)列中移除,然后再維護(hù)等待隊(duì)列中firstWaiter和lastWaiter的指向節(jié)點(diǎn)引用
  • 二、將等待隊(duì)列中移除的節(jié)點(diǎn)追加到同步隊(duì)列尾部,如果同步隊(duì)列追加失敗或者等待隊(duì)列中還存在其他節(jié)點(diǎn)的話,則繼續(xù)循環(huán)喚醒其他節(jié)點(diǎn)的線程
  • 三、加入同步隊(duì)列成功后,如果前驅(qū)節(jié)點(diǎn)狀態(tài)已經(jīng)為結(jié)束狀態(tài)或者在設(shè)置前驅(qū)節(jié)點(diǎn)狀態(tài)為SIGNAL失敗時(shí),直接通過LockSupport.unpark()喚醒節(jié)點(diǎn)內(nèi)的線程

至此,Signal()方法邏輯結(jié)束,不過需要注意的是:我們?cè)诶斫釩ondition的等待/喚醒原理的時(shí)候需要將await()/signal()方法結(jié)合起來理解。在signal()邏輯完成后,被喚醒的線程則會(huì)從前面的await()方法的自旋中退出,因?yàn)楫?dāng)前線程所在的節(jié)點(diǎn)已經(jīng)被移入同步隊(duì)列,所以while (!isOnSyncQueue(node))條件不成立,循環(huán)自然則終止,進(jìn)而被喚醒的線程會(huì)調(diào)用acquireQueued()開始嘗試獲取鎖資源。

六、Condition接口與Monitor對(duì)象等待/喚醒機(jī)制的區(qū)別

最后我們來簡(jiǎn)單的對(duì)比一下ReetrantLock的Condition多條件等待/喚醒機(jī)制與與Synchronized的Monitor對(duì)象鎖等待/喚醒機(jī)制之間的區(qū)別:

對(duì)比項(xiàng) Monitor Condition
前置條件 需持有對(duì)象鎖 需持有獨(dú)占鎖且創(chuàng)建Condition對(duì)象
調(diào)用方式 Object.wait() condition.await類方法都可
隊(duì)列數(shù)量 一個(gè) 多個(gè)
等待時(shí)釋放鎖資源 支持 支持
線程中斷 不支持 支持
超時(shí)中斷 不支持 支持
超時(shí)等待 支持 支持
精準(zhǔn)喚醒線程 不支持 支持
喚醒全部線程 支持 支持

至此AQS獨(dú)占模式的實(shí)現(xiàn)原理分析則告一段落,下篇文章我們?cè)谶M(jìn)一步探究共享模式的具體實(shí)現(xiàn)~
下篇:(六)手撕并發(fā)編程之基于Semaphore與CountDownLatch分析AQS共享模式實(shí)現(xiàn)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容