寫在前面,本文受啟與養兔子的大叔的文章,并在他文章的基礎上添加了一些自己的了解。
AQS(Abstract Queued Synchronzied)
AQS的底層其實就是 “作業系統基礎” 中的狀态量和鎖的V-P操作。
AQS就是JDK中為“線程同步”提供的一套基礎工具類,其實就是一個類而已,其上文講到的同步元件(CountDownLatch,CyclicBarrier,Semaphore)(是以AQS就成了非常重要的一個知識點,因為基于它可以寫出JAVA中的很多“鎖”類。比如此文要分析的ReentrantLock,它就是基于AQS而形成了一個“可重入鎖”。
ReentrantLock
ReetrantLock中的lock方法,底層是調用了sync對象的方法,其中sync繼承了AQS,ReentrantLock中的lock方法調用了sync對象的lock方法,lock方法再調用acquire方法擷取同步狀态,acAquire方法中調用tryAcquire再次擷取同步狀态,如果失敗,則将線程加入以Node為節點形成連結清單實作的隊列中排隊。公平鎖跟非公平鎖的差別在于,擷取同步狀态時,如果狀态為0(可占領鎖的情況下),非公平鎖會讓目前線程嘗試占領該鎖,而公平鎖則需要判斷等待隊列中是否有線程在排隊,沒有排隊的情況下才能讓該線程占領此鎖。
了解ReentrantLock的主要方法
可以看出ReentrantLock對象實作了Lock接口。【這裡借用一下養兔子的大叔的圖及他的思想】
而Lock接口的主要方法有以下幾個:
Lock對象隻是一個接口,上述方法具體的實作其實都在ReentrantLock中,是以我們隻要在ReentrantLock對象中檢視具體實作去了解鎖的“加鎖”和“解鎖”操作是如何做的。
ReentrantLock的主要方法
注意:這裡有個Sync類型的sync屬性,此屬性很重要,下文将會講到
其中加鎖方法即為lock(),解鎖方法即為unLock()
這兩個方法在ReentrantLock 源碼中的實作如下
//lock
public void lock() {
sync.lock();
}
//unlock
public void unlock() {
sync.release(1);
}
從上述可以知道這兩個方法實際上是操作了一個叫做sync的對象,調用該對象的lock和release操作來實作。
sync是什麼東西?
上圖中我們可以看出sync是ReentrantLock中的一個私有的成員變量,且類型是Sync對象。
Sync是什麼類?做啥的?是什麼時候初始化的?
我們先看簡單的“sync是在什麼時候初始化的”
在源碼中,隻有在2個構造函數的地方對sync對象做了初始化,可分别初始化為NonfairSync(非公平)和FairSync(公平)
/** 所有鎖操作都是基于這個字段 */
private final Sync sync;
/**
* 通過該構造函數建立額ReentrantLock是一個非公平鎖
*/
public ReentrantLock() {
sync = new NonfairSync();
}
/**
* 如果入參為true,則建立公平的ReentrantLock;
* 否則,建立非公平鎖
*/
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
這兩個對象(NonfairSync和NonfairSync)也是ReentrantLock的内部類。
繼承關系:
從上圖可以看出FairSync和NonFairSync在類結構上完全一樣且均繼承于Sync
而Sync對象繼承了AQS 其繼承關系如下
ReentrantLock的内部類Sync實作于AQS類
以上我們可以得出這麼一個初步結論
ReentrantLock實作了Lock接口,操作其成員變量sync這個AQS的子類,來完成鎖的相關功能。而sync這個成員變量有2種形态:NonfairSync和FairSync
ReentrantLock的構造函數中,預設的無參構造函數将會把Sync對象建立為NonfairSync對象,這是一個“非公平鎖”;而另一個構造函數ReentrantLock(boolean fair)傳入參數為true時将會把Sync對象建立為“公平鎖”FairSync。
FairSync、NonfairSync、Sync之間的關系
在上文中我們提到ReentrantLock的lock操作是調用sync的lock方法。而sync有2種形态,那麼我們可以分别對比一下NonfairSync的lock方法和FairSync的lock方法有什麼異同。
- 首先是NonfairSync源碼中的lock方法:
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
lock方法使用CAS來嘗試将同步狀态改為1,如果成功則将同步狀态持有線程置為目前線程。否則說明擷取鎖失敗,将調用AQS提供的 acquire()方法。
我們順藤摸瓜,再來看AQS的 acquire()方法:
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
tryAcquire(arg)
:再次嘗試擷取同步狀态,成功直接方法退出,失敗調用addWaiter();
addWaiter(Node.EXCLUSIVE), arg)
:将目前線程以指定模式(獨占式、共享式)封裝為Node節點後置入同步隊列。
2.再來看FairSync源碼中的lock方法。
//沒有嘗試占有該鎖,還是直接調用了AQS的acquire方法。
final void lock() {
acquire(1);
}
//
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
tryAcquire方法了解如下:
通過對比其源碼,我們可以看出其大同小異,底層都是調用了AQS的acquire方法。
FairSync在tryAquire方法中,當判斷到鎖狀态字段state == 0 時,不會立馬将目前線程設定為該鎖的占用線程,而是去判斷是在此線程之前是否有其他線程在等待這個鎖(執行hasQueuedPredecessors()方法),如果是的話,則該線程會加入到等待隊列中,進行排隊(FIFO,先進先出的排隊形式)。這也就是為什麼FairSync可以讓線程之間公平獲得該鎖。
NoFairSync的tryAquire方法中,沒有判斷是否有在此之前的排隊線程,而是直接進行獲鎖操作,是以多個線程之間同時争用一把鎖的時候,誰先擷取到就變得随機了,很有可能線程A比線程B更早等待這把鎖,但是B卻擷取到了鎖,A繼續等待(這種現象叫做:線程饑餓)
線程是什麼時候排隊的?
AQS的源碼 acquire方法
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
這個**addWaiter(Node.EXCLUSIVE)**就是請求排隊的,該動作是在FairSync或者NoFairSync調用AQS的aquire(1)方法時觸發的,而且由該方法中的if塊可知,隻有當if中的tryAcquire為false的時候(也就是獲鎖失敗的時候),才會執行後續的acquireQueued方法。
線程是如何排隊的?
想要知道如何排隊,那也就是去了解**addWaiter(Node.EXCLUSIVE)**這個方法具體是如何實作的。
隊列是連結清單的形式組織不同Node(每一個Node代表一個線程)之間的先後順序。
在AQS中目前線程、線程狀态、此線程的前後節點 被組織到了一個叫做Node的資料結構(内部類),再次不過多贅述,排隊的線程先後組成連結清單,隊列是連結清單的形式,組織不同Node(每一個Node代表一個線程)之間的先後順序。公平鎖依靠這個順序實作公平競争。
等待中的線程如何感覺到鎖空閑并獲得鎖?
簡單地講:就是一個線程擷取鎖失敗了,被放到了線程等待隊列中,而acquireQueued方法就是把放入隊列中的這個線程不斷進行“獲鎖”,直到它**“成功獲鎖”或者“不再需要鎖(如被中斷)” ,是一個自旋**的過程,且為了防止不停的自旋,其自旋過程中添加了一個判斷,判斷目前線程是否需要被阻塞,這樣就防止了自旋空跑。
關于公平與非公平鎖本質上的差別
“不管公平還是非公平模式下,ReentrantLock對于排隊中的線程都能保證,排在前面的一定比排在後面的線程優先獲得鎖”但是,這裡有個但是,非公平模式不保證“隊列中的第一個線程一定就比新來的(未加入到隊列)的線程優先獲鎖”因為隊列中的第一個線程嘗試獲得鎖時,可能剛好來了一個線程也要擷取鎖,而這個剛來的線程都還未加入到等待隊列,此時兩個線程同時随機競争,很有可能,隊列中的第一個線程競争失敗(而該線程等待的時間其實比這個剛來的線程等待時間要久)。
解鎖
在上文中我們可以得出:
隻要有其他線程因為釋放了鎖,那麼“線程等待隊列中的第一個Node節點就可以成功擷取到鎖(如果沒有隊列外的線程同時競争這個鎖)”
實際上這并不是一個很準确的結論,因為“線程等待隊列”中的第一個Node節點在其他線程未釋放鎖時,因為擷取不到鎖,那麼也會被“阻塞”
這個時候,實際上所有在等待隊列中的Node節點裡代表的線程都是處于“阻塞”狀态。
那什麼時候喚醒這些阻塞的線程呢?
既然申請鎖的時候會導緻線程在得不到鎖時被“阻塞”,那麼,肯定就是其他線程在**釋放鎖時“喚醒”**被阻塞着的線程去“拿鎖”。
ReentrantLock中的源碼
public void unlock() {
sync.release(1);
}
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
其中,unparkSuccessor(h)方法就是“喚醒操作”,主要流程如代碼所示
- 嘗試釋放目前線程持有的鎖
- 如果成功釋放,那麼去喚醒頭結點的後繼節點(因為頭節點head是不儲存線程資訊的節點,僅僅是因為資料結構設計上的需要,在資料結構上,這種做法往往叫做“空頭節點連結清單”。對應的就有“非空頭結點連結清單”)
unparkSuccessor(h)的執行流程源碼解析:
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);
//如果head節點的下一個節點它是null或者已經被cancelled了(status>0)
//那麼就從隊列的尾巴往前找,找到一個最前面的并且狀态不是cancelled的線程
//至于為什麼要從後往前找,不是從前往後找,誰能跟我說一下,這點我也不知道為什麼
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)
LockSupport.unpark(s.thread);
}
至此,申請鎖的“阻塞”和釋放鎖的“喚醒”操作中**“隊列什麼時候進行排隊”、“如何排隊”、“什麼時候移除隊列”、“何時阻塞線程”**、“何時喚醒線程”基本都已經解釋清楚了。
如何實作可重入
加鎖操作會對state字段進行+1操作,這裡需要注意到AQS中很多内部變量的修飾符都是采用的volitale,然後配合CAS操作來保證AQS本身的線程安全(因為AQS自己線程安全,基于它的衍生類才能更好地保證線程安全)。
這裡的state字段就是AQS類中的一個用volitale修飾的int變量,state字段初始化時,值為0。表示目前沒有任何線程持有該鎖。
當一個線程每次獲得該鎖時,值就會在原來的基礎上加1,多次獲鎖就會多次加1(指同一個線程),這裡就是可重入。因為可以同一個線程多次獲鎖,隻是對這個字段的值在原來基礎上加1。
相反unlock操作也就是解鎖操作,實際是是調用AQS的release操作,而每執行一次這個操作,就會對state字段在原來的基礎上減1,當state==0的時候就表示目前線程已經完全釋放了該鎖。那麼就會如上文提到的那樣去調用“喚醒”動作,去把在“線程等待隊列中的線程”叫醒。