引言
在我們前面的文章《深入理解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)。
全局變量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):
- 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é)一下整體的流程圖:
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)原理如下:
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)如下:
如上圖,與同步隊(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)