前言
這篇文章時JAVA併發專欄的第四篇,在此特意感謝以下作者的精彩分享:
1. 初識Lock與AbstractQueuedSynchronizer(AQS)
2. 深入理解AbstractQueuedSynchronizer(AQS)
1. JUC的基本結構
Java開發人員中,我們一般把java.util.concurrent簡稱爲JUC包,泛指我們日常用到的併發多線程中的知識模塊。下圖是concurrent包的目錄結構圖。
其中包含了兩個子包:atomic以及lock,另外在concurrent下的阻塞隊列以及executors,這些就是concurrent包中的精華。而這些類的實現主要是依賴於volatile以及CAS,從整體上來看concurrent包的整體實現圖如下圖所示:
2. 初識Lock 與ReentractLock
鎖是用來控制多個線程訪問共享資源的方式,一般來說,一個鎖能夠防止多個線程同時訪問共享資源。在Lock接口出現之前,java程序主要是靠synchronized關鍵字實現鎖功能的,而java SE5之後,併發包中增加了lock接口,它提供了與synchronized一樣的鎖功能。雖然它失去了像synchronize關鍵字隱式加鎖解鎖的便捷性,但是卻擁有了鎖獲取和釋放的可操作性,可中斷的獲取鎖,公平鎖等多種synchronized關鍵字所不具備的同步特性。 通常使用顯示使用lock的形式如下:
Lock lock = new ReentrantLock();
lock.lock();
try{
.......
}finally{
lock.unlock();
}
synchronized同步塊執行完成或者遇到異常是鎖會自動釋放,而lock必須調用unlock()方法釋放鎖,因此在finally塊中釋放鎖。
1. Lock接口的主要方法
我們下面介紹Lock接口中的主要方法:
- void lock(): 執行此方法時, 如果鎖處於空閒狀態, 當前線程將獲取到鎖. 相反, 如果鎖已經被其他線程持有, 將禁用當前線程, 直到當前線程獲取到鎖.
- boolean tryLock():如果鎖可用, 則獲取鎖, 並立即返回true, 否則返回false. 該方法和lock()的區別在於, tryLock()只是"試圖"獲取鎖, 如果鎖不可用, 不會導致當前線程被禁用, 當前線程仍然繼續往下執行代碼. 而lock()方法則是一定要獲取到鎖, 如果鎖不可用, 就一直等待, 在未獲得鎖之前,當前線程並不繼續向下執行.
- void unlock():執行此方法時, 當前線程將釋放持有的鎖. 鎖只能由持有者釋放, 如果線程並不持有鎖, 卻執行該方法, 可能導致異常的發生.
- Condition newCondition():條件對象,獲取等待通知組件。該組件和當前的鎖綁定,當前線程只有獲取了鎖,才能調用該組件的await()方法,而調用後,當前線程將縮放鎖。
- getHoldCount() :查詢當前線程保持此鎖的次數,也就是執行此線程執行lock方法的次數。
- getQueueLength():返回正等待獲取此鎖的線程估計數,比如啓動10個線程,1個線程獲得鎖,此時返回的是9
- getWaitQueueLength:(Condition condition)返回等待與此鎖相關的給定條件的線程估計數。比如10個線程,用同一個condition對象,並且此時這10個線程都執行了condition對象的await方法,那麼此時執行此方法返回10
- hasWaiters(Condition condition):查詢是否有線程等待與此鎖有關的給定條件(condition),對於指定contidion對象,有多少線程執行了condition.await方法
- hasQueuedThread(Thread thread):查詢給定線程是否等待獲取此鎖
- hasQueuedThreads():是否有線程等待此鎖
- isFair():該鎖是否公平鎖
- isHeldByCurrentThread(): 當前線程是否保持鎖鎖定,線程的執行lock方法的前後分別是false和true
- isLock():此鎖是否有任意線程佔用
- lockInterruptibly():如果當前線程未被中斷,獲取鎖
- tryLock():嘗試獲得鎖,僅在調用時鎖未被線程佔用,獲得鎖
- tryLock(long timeout TimeUnit unit):如果鎖在給定等待時間內沒有被另一個線程保持,則獲取該鎖。
2. ReentrantLock
接下來我們看看著名的ReentrantLock:
public class ReentrantLock implements Lock, java.io.Serializable
很顯然ReentrantLock實現了lock接口,接下來我們來仔細研究一下它是怎樣實現的。當你查看源碼時你會驚訝的發現ReentrantLock並沒有多少代碼,另外有一個很明顯的特點是:基本上所有的方法的實現實際上都是調用了其靜態內存類Sync中的方法,而Sync類繼承了AbstractQueuedSynchronizer(AQS)。可以看出要想理解ReentrantLock關鍵核心在於對隊列同步器AbstractQueuedSynchronizer(簡稱同步器)的理解。
非公平鎖
JVM按隨機、就近原則分配鎖的機制則稱爲不公平鎖,ReentrantLock在構造函數中提供了是否公平鎖的初始化方式,默認爲非公平鎖。非公平鎖實際執行的效率要遠遠超出公平鎖,除非程序有特殊需要,否則最常用非公平鎖的分配機制。
公平鎖
公平鎖指的是鎖的分配機制是公平的,通常先對鎖提出獲取請求的線程會先被分配到鎖,ReentrantLock在構造函數中提供了是否公平鎖的初始化方式來定義公平鎖。
最後南國再次強調一下ReentrantLock 與synchronized 之間的區別:
- 鎖的實現:ReentrantLock是基於juc實現,synchronized是基於jvm實現。ReentrantLock通過方法lock()與unlock()來進行加鎖與解鎖操作,與synchronized會被JVM自動解鎖機制不同,ReentrantLock加鎖後需要手動進行解鎖。爲了避免程序出現異常而無法正常解鎖的情況,使用ReentrantLock必須在finally控制塊中進行解鎖操作。
- ReentrantLock相比synchronized的優勢是可中斷、公平鎖、多個鎖。這種情況下需要使用ReentrantLock。
3. 初識AQS
在前面,南國提到理解RenetrantLock的核心在於深入理解AQS(AbstractQueueSynchornizer 抽象隊列同步器),它定義了以套多線程訪問共享資源的同步器框架,許多同步器的實現都基於AQS,包括常用的ReentrantLock, Semaphare, CountDownLatch.
它維護了一個volatile int state(代表共享資源)和一個FIFO線程等待隊列(多線程爭用資源被阻塞時會進入此隊列)。它的子類必須重寫AQS的幾個protected修飾的用來改變同步狀態的方法,其他方法主要是實現了排隊和阻塞機制。狀態的更新使用getState,setState以及compareAndSetState這三個方法。
子類被推薦定義爲自定義同步組件的靜態內部類,同步器自身沒有實現任何同步接口,它僅僅是定義了若干同步狀態的獲取和釋放方法來供自定義同步組件的使用,同步器既支持獨佔式獲取同步狀態,也可以支持共享式獲取同步狀態,這樣就可以方便的實現不同類型的同步組件。
同步器是實現鎖(也可以是任意同步組件)的關鍵,在鎖的實現中聚合同步器,利用同步器實現鎖的語義。可以這樣理解二者的關係:鎖是面向使用者,它定義了使用者與鎖交互的接口,隱藏了實現細節;同步器是面向鎖的實現者,它簡化了鎖的實現方式,屏蔽了同步狀態的管理,線程的排隊,等待和喚醒等底層操作。鎖和同步器很好的隔離了使用者和實現者所需關注的領域。
AQS定義兩種資源共享方式:
- Exclusive獨佔資源-ReentrantLock Exclusive(獨佔,只有一個線程能執行,如ReentrantLock)
- Share共享資源-Semaphore/CountDownLatch Share(共享,多個線程可同時執行,如Semaphore/CountDownLatch)。
AQS的模板方法設計模式
AQS的設計是使用模板方法設計模式,它將一些方法開放給子類進行重寫,而同步器給同步組件所提供模板方法又會重新調用被子類所重寫的方法。舉個例子,AQS中需要重寫的方法tryAcquire:
protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}
ReentrantLock中NonfairSync(繼承AQS)會重寫該方法爲:
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
而AQS中的模板方法acquire():
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
會調用tryAcquire方法,而此時當繼承AQS的NonfairSync調用模板方法acquire時就會調用已經被NonfairSync重寫的tryAcquire方法。這就是使用AQS的方式,在弄懂這點後會lock的實現理解有很大的提升。可以歸納總結爲這麼幾點:
- 同步組件(這裏不僅僅指鎖,還包括CountDownLatch等)的實現依賴於同步器AQS,在同步組件實現中,使用AQS的方式被推薦定義繼承AQS的靜態內存類;
- AQS採用模板方法進行設計,AQS的protected修飾的方法需要由繼承AQS的子類進行重寫實現,當調用AQS的子類的方法時就會調用被重寫的方法;
- AQS負責同步狀態的管理,線程的排隊,等待和喚醒這些底層操作,而Lock等同步組件主要專注於實現同步語義;
- 在重寫AQS的方式時,使用AQS提供的getState(),setState(),compareAndSetState()方法進行修改同步狀態
AQS可重寫的方法如下:
在實現同步組件時AQS提供的模板方法如下圖:
總結:AQS提供的模板方法可以分爲3類:
- 獨佔式獲取與釋放同步狀態;
- 共享式獲取與釋放同步狀態;
- 查詢同步隊列中等待線程情況;
同步組件通過AQS提供的模板方法實現自己的同步語義。
AQS案例
通過下面這個案例實現了獨佔鎖的語義,在同一個時刻只允許一個線程佔有鎖。
package 併發多線程.Lock_AQS_demo;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.AbstractQueuedSynchronizer;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
/**
* AQS的一個簡單例子(實現了獨佔鎖的語義,在同一時刻只允許一個線程佔有鎖)
* @author xjh 2019.12.16
*/
class Mutex implements Lock, java.io.Serializable {
// Our internal helper class
// 繼承AQS的靜態內存類
// 重寫方法
private static class Sync extends AbstractQueuedSynchronizer {
// Reports whether in locked state
protected boolean isHeldExclusively() { //判斷該線程是否在獨佔資源
return getState() == 1;
}
// Acquires the lock if state is zero
public boolean tryAcquire(int acquires) { //獨佔鎖獲取同步狀態,成功則返回true
assert acquires == 1; // Otherwise unused
if (compareAndSetState(0, 1)) {
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
return false;
}
// Releases the lock by setting state to zero
protected boolean tryRelease(int releases) {//獨佔鎖釋放資源
assert releases == 1; // Otherwise unused
if (getState() == 0) throw new IllegalMonitorStateException();
setExclusiveOwnerThread(null);
setState(0);
return true;
}
// Provides a Condition
Condition newCondition() {
return new ConditionObject();
}
// Deserializes properly
private void readObject(ObjectInputStream s)
throws IOException, ClassNotFoundException {
s.defaultReadObject();
setState(0); // reset to unlocked state
}
}
// The sync object does all the hard work. We just forward to it.
private final Sync sync = new Sync();
//使用同步器的模板方法實現自己的同步語義
public void lock() {
sync.acquire(1);
}
public boolean tryLock() {
return sync.tryAcquire(1);
}
public void unlock() {
sync.release(1);
}
public Condition newCondition() {
return sync.newCondition();
}
public boolean isLocked() {
return sync.isHeldExclusively();
}
public boolean hasQueuedThreads() {
return sync.hasQueuedThreads();
}
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
public boolean tryLock(long timeout, TimeUnit unit)
throws InterruptedException {
return sync.tryAcquireNanos(1, unit.toNanos(timeout));
}
}
public class Mutex_demo {
private static Mutex mutex=new Mutex();
public static void main(String[] args) {
for (int i=0;i<10;i++){
Thread thread=new Thread(()->{
mutex.lock();
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
mutex.unlock();
}
});
thread.start();
}
}
}
這個demo因爲沒有寫輸出,所以正常運行看不出什麼東西,我們在mian函數 for循環一行 和thread.start()分別打上斷點進入debug模式。從圖中可以看到某一時刻只有一個線程在運行。
同步隊列
當共享資源被某個線程佔有,其他請求該資源的線程將會阻塞,從而進入同步隊列。就數據結構而言,隊列的實現方式無外乎兩者一是通過數組的形式,另外一種則是鏈表的形式。
AQS中的同步隊列則是通過鏈式方式進行實現
接下來,很顯然我們至少會抱有這樣的疑問:**1. 節點的數據結構是什麼樣的?2. 是單向還是雙向?3. 是帶頭結點的還是不帶頭節點的?**我們依舊先是通過看源碼的方式。
在AQS有一個靜態內部類Node,其中有這樣一些屬性:
volatile int waitStatus //節點狀態
volatile Node prev //當前節點/線程的前驅節點
volatile Node next; //當前節點/線程的後繼節點
volatile Thread thread;//加入同步隊列的線程引用
Node nextWaiter;//等待隊列中的下一個節點
節點的狀態有以下這些:
int CANCELLED = 1 //節點從同步隊列中取消
int SIGNAL = -1 //後繼節點的線程處於等待狀態,如果當前節點釋放同步狀態會通知後繼節點,使得後繼節點的線程能夠運行;
int CONDITION = -2 //當前節點進入等待隊列中
int PROPAGATE = -3 //表示下一次共享式同步狀態獲取將會無條件傳播下去
int INITIAL = 0; //初始狀態
現在我們知道了節點的數據結構類型,並且每個節點擁有其前驅和後繼節點,很顯然這是一個雙向隊列。
也就是說AQS實際上通過頭尾指針來管理同步隊列,同時實現包括獲取鎖失敗的線程進行入隊,釋放鎖時對同步隊列中的線程進行通知等核心方法。其示意圖如下:
通過對源碼的理解,現在我們可以清楚的知道這樣幾點:
1.節點的數據結構,即AQS的靜態內部類Node,節點的等待狀態等信息;
2. 同步隊列是一個雙向隊列,AQS通過持有頭尾指針管理同步隊列;
那麼,節點如何進行入隊和出隊是怎樣做的了?實際上這對應着鎖的獲取和釋放兩個操作:獲取鎖失敗進行入隊操作,獲取鎖成功進行出隊操作。
獨佔鎖(Exclusive)
1. 獨佔鎖的獲取(acquire方法)
通過調用lock()方法是獲取獨佔式鎖,獲取失敗就將當前線程加入同步隊列,成功則線程執行。而lock()方法實際上會調用AQS的**acquire()**方法,源碼如下:
public final void acquire(int arg) {
//先看同步狀態是否獲取成功,如果成功則方法結束返回
//若失敗則先調用addWaiter()方法再調用acquireQueued()方法
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
關鍵信息請看註釋,acquire根據當前獲得同步狀態成功與否做了兩件事情:
- 成功,則方法結束返回,
- 失敗,則先調用addWaiter()然後在調用acquireQueued()方法。
1. 獲取同步狀態失敗,入隊操作
當線程獲取獨佔式鎖失敗後就會將當前線程加入同步隊列,那麼加入隊列的方式是怎樣的了?我們接下來就應該去研究一下addWaiter()和acquireQueued()。addWaiter()源碼如下:
private Node addWaiter(Node mode) {
// 1. 將當前線程構建成Node類型
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
// 2. 當前尾節點是否爲null?
Node pred = tail;
if (pred != null) {
// 2.2 將當前節點尾插入的方式插入同步隊列中
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
// 2.1. 當前同步隊列尾節點爲null,說明當前線程是第一個加入同步隊列進行等待的線程
enq(node);
return node;
}
分析可以看上面的註釋。程序的邏輯主要分爲兩個部分:1. 當前同步隊列的尾節點爲null,調用方法enq()插入;2. 當前隊列的尾節點不爲null,則採用尾插入(compareAndSetTail()方法)的方式入隊。另外還會有另外一個問題:如果 if (compareAndSetTail(pred, node))爲false怎麼辦?會繼續執行到enq()方法,同時很明顯compareAndSetTail是一個CAS操作,通常來說如果CAS操作失敗會繼續自旋(死循環)進行重試。因此,經過我們這樣的分析,enq()方法可能承擔兩個任務:1. 處理當前同步隊列尾節點爲null時進行入隊操作;2. 如果CAS尾插入節點失敗後負責自旋進行嘗試。
那麼是不是真的就像我們分析的一樣了?只有源碼會告訴我們答案:),enq()源碼如下:
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) { // Must initialize
//1. 構造頭結點
if (compareAndSetHead(new Node()))
tail = head;
} else {
// 2. 尾插入,CAS操作失敗自旋嘗試
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
在上面的分析中我們可以看出在第1步中會先創建頭結點,說明同步隊列是帶頭結點的鏈式存儲結構。帶頭結點與不帶頭結點相比,會在入隊和出隊的操作中獲得更大的便捷性,因此同步隊列選擇了帶頭結點的鏈式存儲結構。那麼帶頭節點的隊列初始化時機是什麼?自然而然是在tail爲null時,即當前線程是第一次插入同步隊列。compareAndSetTail(t, node)方法會利用CAS操作設置尾節點,如果CAS操作失敗會在for (;;)for死循環中不斷嘗試,直至成功return返回爲止。因此,對enq()方法可以做這樣的總結:
- 在當前線程是第一個加入同步隊列時,調用compareAndSetHead(new Node())方法,完成鏈式隊列的頭結點的初始化;
- 自旋不斷嘗試CAS尾插入節點直至成功爲止。
現在我們已經很清楚獲取獨佔式鎖失敗的線程包裝成Node 然後插入同步隊列的過程了?那麼緊接着會有下一個問題?在同步隊列中的節點(線程)會做什麼事情了來保證自己能夠有機會獲得獨佔式鎖了?帶着這樣的問題我們就來看看acquireQueued()方法,從方法名就可以很清楚,這個方法的作用就是排隊獲取鎖的過程,源碼如下:
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
// 1. 獲得當前節點的先驅節點
final Node p = node.predecessor();
// 2. 當前節點能否獲取獨佔式鎖
// 2.1 如果當前節點的先驅節點是頭結點並且成功獲取同步狀態,即可以獲得獨佔式鎖
if (p == head && tryAcquire(arg)) {
//隊列頭指針用指向當前節點
setHead(node);
//釋放前驅節點
p.next = null; // help GC
failed = false;
return interrupted;
}
// 2.2 獲取鎖失敗,線程進入等待狀態等待獲取獨佔式鎖
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
程序邏輯通過註釋已經標出,整體來看這是一個這又是一個自旋的過程(for (; ;)),代碼首先獲取當前節點的先驅節點,如果先驅節點是頭結點的並且成功獲得同步狀態的時候(if (p == head && tryAcquire(arg))),當前節點所指向的線程能夠獲取鎖。反之,獲取鎖失敗進入等待狀態。整體示意圖爲下圖:
2. 獲取鎖成功,出隊操作
獲取鎖的節點出隊的邏輯是:
//隊列頭結點引用指向當前節點
setHead(node);
//釋放前驅節點
p.next = null; // help GC
failed = false;
return interrupted;
setHead()方法爲:
private void setHead(Node node) {
head = node;
node.thread = null;
node.prev = null;
}
將當前節點通過setHead()方法設置爲隊列的頭結點,然後將之前的頭結點的next域設置爲null並且pre域也爲null,即與隊列斷開,無任何引用方便GC時能夠將內存進行回收。示意圖如下:
那麼當獲取鎖失敗的時候會調用shouldParkAfterFailedAcquire()方法和parkAndCheckInterrupt()方法,看看他們做了什麼事情。shouldParkAfterFailedAcquire()方法源碼爲:
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
/*
* This node has already set status asking a release
* to signal it, so it can safely park.
*/
return true;
if (ws > 0) {
/*
* Predecessor was cancelled. Skip over predecessors and
* indicate retry.
*/
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
/*
* waitStatus must be 0 or PROPAGATE. Indicate that we
* need a signal, but don't park yet. Caller will need to
* retry to make sure it cannot acquire before parking.
*/
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
shouldParkAfterFailedAcquire()方法主要邏輯是使用compareAndSetWaitStatus(pred, ws, Node.SIGNAL)使用CAS將節點狀態由INITIAL設置成SIGNAL,表示當前線程阻塞。當compareAndSetWaitStatus設置失敗則說明shouldParkAfterFailedAcquire方法返回false,然後會在acquireQueued()方法中for (;;)死循環中會繼續重試,直至compareAndSetWaitStatus設置節點狀態位爲SIGNAL時shouldParkAfterFailedAcquire返回true時纔會執行方法parkAndCheckInterrupt()方法,該方法的源碼爲:
private final boolean parkAndCheckInterrupt() {
//使得該線程阻塞
LockSupport.park(this);
return Thread.interrupted();
}
該方法的關鍵是會調用LookSupport.park()方法(關於LookSupport會在以後的文章進行討論),該方法是用來阻塞當前線程的。因此到這裏就應該清楚了,acquireQueued()在自旋過程中主要完成了兩件事情:
- 如果當前節點的前驅節點是頭節點,並且能夠獲得同步狀態的話,當前線程能夠獲得鎖該方法執行結束退出;
- 獲取鎖失敗的話,先將節點狀態設置成SIGNAL,然後調用LookSupport.park方法使得當前線程阻塞。
經過上面的分析,獨佔式鎖的獲取過程也就是acquire()方法的執行流程如下圖所示:
2. 獨佔鎖的釋放(release()方法)
獨佔鎖的釋放就相對來說比較容易理解了,廢話不多說先來看下源碼:
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
這段代碼邏輯就比較容易理解了,如果同步狀態釋放成功(tryRelease返回true)則會執行if塊中的代碼,當head指向的頭結點不爲null,並且該節點的狀態值不爲0的話纔會執行unparkSuccessor()方法。unparkSuccessor方法源碼:
private void unparkSuccessor(Node node) {
/*
* If status is negative (i.e., possibly needing signal) try
* to clear in anticipation of signalling. It is OK if this
* fails or if status is changed by waiting thread.
*/
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
/*
* Thread to unpark is held in successor, which is normally
* just the next node. But if cancelled or apparently null,
* traverse backwards from tail to find the actual
* non-cancelled successor.
*/
//頭節點的後繼節點
Node s = node.next;
if (s == null || s.waitStatus > 0) {
s = null;
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
if (s != null)
//後繼節點不爲null時喚醒該線程
LockSupport.unpark(s.thread);
}
源碼的關鍵信息請看註釋,首先獲取頭節點的後繼節點,當後繼節點的時候會調用LookSupport.unpark()方法,該方法會喚醒該節點的後繼節點所包裝的線程。因此,每一次鎖釋放後就會喚醒隊列中該節點的後繼節點所引用的線程,從而進一步可以佐證獲得鎖的過程是一個FIFO(先進先出)的過程。
到現在我們終於啃下了一塊硬骨頭了,通過學習源碼的方式非常深刻的學習到了獨佔式鎖的獲取和釋放的過程以及同步隊列。可以做一下總結:
- 線程獲取鎖失敗,線程被封裝成Node進行入隊操作,核心方法在於addWaiter()和enq(),同時enq()完成對同步隊列的頭結點初始化工作以及CAS操作失敗的重試;
- 線程獲取鎖是一個自旋的過程,當且僅當 當前節點的前驅節點是頭結點並且成功獲得同步狀態時,節點出隊即該節點引用的線程獲得鎖,否則,當不滿足條件時就會調用LookSupport.park()方法使得線程阻塞;
- 釋放鎖的時候會喚醒後繼節點;
總體來說:在獲取同步狀態時,AQS維護一個同步隊列,獲取同步狀態失敗的線程會加入到隊列中進行自旋;移除隊列(或停止自旋)的條件是前驅節點是頭結點並且成功獲得了同步狀態。在釋放同步狀態時,同步器會調用unparkSuccessor()方法喚醒後繼節點。
3. 可中斷式獲取鎖(acquireInterruptibly方法)
我們知道lock相較於synchronized有一些更方便的特性,比如能響應中斷以及超時等待等特性,現在我們依舊採用通過學習源碼的方式來看看能夠響應中斷是怎麼實現的。可響應中斷式鎖可調用方法lock.lockInterruptibly();而該方法其底層會調用AQS的acquireInterruptibly方法,源碼爲:
public final void acquireInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
if (!tryAcquire(arg))
//線程獲取鎖失敗
doAcquireInterruptibly(arg);
}
在獲取同步狀態失敗後就會調用doAcquireInterruptibly方法:
private void doAcquireInterruptibly(int arg)
throws InterruptedException {
//將節點插入到同步隊列中
final Node node = addWaiter(Node.EXCLUSIVE);
boolean failed = true;
try {
for (;;) {
final Node p = node.predecessor();
//獲取鎖出隊
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
//線程中斷拋異常
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}
關鍵信息請看註釋,現在看這段代碼就很輕鬆了吧:),與acquire方法邏輯幾乎一致,唯一的區別是當parkAndCheckInterrupt返回true時即線程阻塞時該線程被中斷,代碼拋出被中斷異常。
4. 超時等待式獲取鎖(tryAcquireNanos()方法)
通過調用lock.tryLock(timeout,TimeUnit)方式達到超時等待獲取鎖的效果,該方法會在三種情況下才會返回:
- 在超時時間內,當前線程成功獲取了鎖;
- 當前線程在超時時間內被中斷;
- 超時時間結束,仍未獲得鎖返回false。
我們仍然通過採取閱讀源碼的方式來學習底層具體是怎麼實現的,該方法會調用AQS的方法tryAcquireNanos(),源碼爲:
public final boolean tryAcquireNanos(int arg, long nanosTimeout)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
return tryAcquire(arg) ||
//實現超時等待的效果
doAcquireNanos(arg, nanosTimeout);
}
很顯然這段源碼最終是靠doAcquireNanos方法實現超時等待的效果,該方法源碼如下:
private boolean doAcquireNanos(int arg, long nanosTimeout)
throws InterruptedException {
if (nanosTimeout <= 0L)
return false;
//1. 根據超時時間和當前時間計算出截止時間
final long deadline = System.nanoTime() + nanosTimeout;
final Node node = addWaiter(Node.EXCLUSIVE);
boolean failed = true;
try {
for (;;) {
final Node p = node.predecessor();
//2. 當前線程獲得鎖出隊列
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return true;
}
// 3.1 重新計算超時時間
nanosTimeout = deadline - System.nanoTime();
// 3.2 已經超時返回false
if (nanosTimeout <= 0L)
return false;
// 3.3 線程阻塞等待
if (shouldParkAfterFailedAcquire(p, node) &&
nanosTimeout > spinForTimeoutThreshold)
LockSupport.parkNanos(this, nanosTimeout);
// 3.4 線程被中斷拋出被中斷異常
if (Thread.interrupted())
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}
程序邏輯如圖所示:
程序邏輯同獨佔鎖可響應中斷式獲取基本一致,唯一的不同在於獲取鎖失敗後,對超時時間的處理上,在第1步會先計算出按照現在時間和超時時間計算出理論上的截止時間,比如當前時間是8h10min,超時時間是10min,那麼根據deadline = System.nanoTime() + nanosTimeout計算出剛好達到超時時間時的系統時間就是8h 10min+10min = 8h 20min。然後根據deadline - System.nanoTime()就可以判斷是否已經超時了,比如,當前系統時間是8h 30min很明顯已經超過了理論上的系統時間8h 20min,deadline - System.nanoTime()計算出來就是一個負數,自然而然會在3.2步中的If判斷之間返回false。如果還沒有超時即3.2步中的if判斷爲true時就會繼續執行3.3步通過LockSupport.parkNanos使得當前線程阻塞,同時在3.4步增加了對中斷的檢測,若檢測出被中斷直接拋出被中斷異常。
共享鎖
1 共享鎖的獲取(acquireShared()方法)
在聊完AQS對獨佔鎖的實現後,我們繼續一鼓作氣的來看看共享鎖是怎樣實現的?共享鎖的獲取方法爲acquireShared,源碼爲:
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
這段源碼的邏輯很容易理解,在該方法中會首先調用tryAcquireShared方法,tryAcquireShared返回值是一個int類型,當返回值爲大於等於0的時候方法結束說明獲得成功獲取鎖,否則,表明獲取同步狀態失敗即所引用的線程獲取鎖失敗,會執行doAcquireShared方法,該方法的源碼爲:
private void doAcquireShared(int arg) {
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head) {
int r = tryAcquireShared(arg);
if (r >= 0) {
// 當該節點的前驅節點是頭結點且成功獲取同步狀態
setHeadAndPropagate(node, r);
p.next = null; // help GC
if (interrupted)
selfInterrupt();
failed = false;
return;
}
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
現在來看這段代碼會不會很容易了?邏輯幾乎和獨佔式鎖的獲取一模一樣,這裏的自旋過程中能夠退出的條件是當前節點的前驅節點是頭結點並且tryAcquireShared(arg)返回值大於等於0即能成功獲得同步狀態。
2. 共享鎖的釋放(releaseShared()方法)
共享鎖的釋放在AQS中會調用方法releaseShared:
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
當成功釋放同步狀態之後即tryReleaseShared會繼續執行doReleaseShared方法:
private void doReleaseShared() {
/*
* Ensure that a release propagates, even if there are other
* in-progress acquires/releases. This proceeds in the usual
* way of trying to unparkSuccessor of head if it needs
* signal. But if it does not, status is set to PROPAGATE to
* ensure that upon release, propagation continues.
* Additionally, we must loop in case a new node is added
* while we are doing this. Also, unlike other uses of
* unparkSuccessor, we need to know if CAS to reset status
* fails, if so rechecking.
*/
for (;;) {
Node h = head;
if (h != null && h != tail) {
int ws = h.waitStatus;
if (ws == Node.SIGNAL) {
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue; // loop to recheck cases
unparkSuccessor(h);
}
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue; // loop on failed CAS
}
if (h == head) // loop if head changed
break;
}
}
這段方法跟獨佔式鎖釋放過程有點點不同,在共享式鎖的釋放過程中,對於能夠支持多個線程同時訪問的併發組件,必須保證多個線程能夠安全的釋放同步狀態,這裏採用的CAS保證,當CAS操作失敗continue,在下一次循環中進行重試。
3. 可中斷(acquireSharedInterruptibly()方法),超時等待(tryAcquireSharedNanos()方法)
關於可中斷鎖以及超時等待的特性其實現和獨佔式鎖可中斷獲取鎖以及超時等待的實現幾乎一致,具體的就不再說了,如果理解了上面的內容對這部分的理解也是水到渠成的。