前言:
Java中的同步類ReentrantLock是基於AbstractQueuedSynchronizer(簡稱爲AQS)實現的。
今天從源碼來了解下ReentrantLock中非公平鎖的加鎖和釋放鎖(ReentrantLock中支持公平鎖和非公平鎖,默認是非公平鎖的,但可以通過創建ReentrantLock對象時傳入參數指定使用公平鎖)。
在瞭解ReentrantLock前,需要對AQS有一定的瞭解,否則在學習時會比較困難的,並且在通過源碼學習ReentrantLock時也會穿插着講解AQS內容。
AQS掃蕩:
1.0、AQS中state變量
AQS中提供了一個int類型的state變量,並且state變量被volatile修飾,表示state變量的讀寫操作可以保證原子性;並且AQS還提供了針對state變量的讀寫方法,以及使用CAS算法更新state變量的方法。 AQS使用state變量這個狀態變量來實現同步狀態。
①、源碼展示
/**
* The synchronization state.
*/
private volatile int state;
/**
* get 獲取state變量值
*/
protected final int getState() {
return state;
}
/**
* set 更新state變量值
* @param newState 新的狀態變量值
*/
protected final void setState(int newState) {
state = newState;
}
/**
* 使用CAS算法更新state變量值; 當從共享內存中讀取出的state變量值與expect期望值一致的話,
* 就將其更新爲update值。使用CAS算法保證其操作的原子性
*
* @param expect 期望值
* @param update 更新值
*/
protected final boolean compareAndSetState(int expect, int update) {
// 使用Unsafe類的本地方法來實現CAS
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
1.1、state同步狀態的競爭
多個線程同時競爭AQS的state同步狀態,在同一時刻只能有一個線程獲取到同步狀態(獲取到鎖),那其它沒獲取到鎖的線程該怎麼辦呢
它們會進去到一個同步隊列中,在隊列中等待同步鎖的釋放;
這個同步隊列是一個基於鏈表的雙向隊列 , 基於鏈表的話,就會存在Node節點,那麼AQS中節點是怎麼實現的呢
①、Node節點:
AQS中自己實現了一個內部Node節點類,Node節點類中定義了一些屬性,下面來簡單說說屬性的意思:
static final class Node {
// 標誌在同步隊列中Node節點的模式,共享模式
static final Node SHARED = new Node();
// 標誌在同步隊列中Node節點的模式,獨佔(排他)模式
static final Node EXCLUSIVE = null;
// waitStatus值爲1時表示該線程節點已釋放(超時等),已取消的節點不會再阻塞。
static final int CANCELLED = 1;
// waitStatus值爲-1時表示當此節點的前驅結點釋放鎖時,然後當前節點中的線程就可以去獲取鎖運行
static final int SIGNAL = -1;
/**
* waitStatus爲-2時,表示該線程在condition隊列中阻塞(Condition有使用),
* 當其他線程調用了Condition的signal()方法後,CONDITION狀態的結點將從
* 等待隊列轉移到同步隊列中,等待獲取同步鎖。
*/
static final int CONDITION = -2;
/**
* waitStatus爲-3時,與共享模式有關,在共享模式下,該狀態表示可運行
* (CountDownLatch中有使用)。
*/
static final int PROPAGATE = -3;
/**
* waitStatus:等待狀態,指的是當前Node節點中存放的線程的等待狀態,
* 等待狀態值就是上面的四個狀態值:CANCELLED、SIGNAL、CONDITION、PROPAGATE
*/
volatile int waitStatus;
/**
* 因爲同步隊列是雙向隊列,那麼每個節點都會有指向前一個節點的 prev 指針
*/
volatile Node prev;
/**
* 因爲同步隊列是雙向隊列,那麼每個節點也都會有指向後一個節點的 next 指針
*/
volatile Node next;
/**
* Node節點中存放的阻塞的線程引用
*/
volatile Thread thread;
/**
* 當前節點與其next後繼結點的所屬模式,是SHARED共享模式,還是EXCLUSIVE獨佔模式,
*
* 注:比如說當前節點A是共享的,那麼它的這個字段是shared,也就是說在這個等待隊列中,
* A節點的後繼節點也是shared。
*/
Node nextWaiter;
/**
* 獲取當前節點是否爲共享模式
*/
final boolean isShared() {
return nextWaiter == SHARED;
}
/**
* 獲取當前節點的 prev前驅結點
*/
final Node predecessor() throws NullPointerException {
Node p = prev;
if (p == null)
throw new NullPointerException();
else
return p;
}
Node() { }
// 在後面的addWaiter方法會使用到,線程競爭state同步鎖失敗時,會創建Node節點存放thread
Node(Thread thread, Node mode) {
this.nextWaiter = mode;
this.thread = thread;
}
Node(Thread thread, int waitStatus) { // Used by Condition
this.waitStatus = waitStatus;
this.thread = thread;
}
}
②、同步隊列結構圖(雙向隊列):
1.2、圖解AQS原理
通過前面兩點,可以瞭解到AQS的原理到底是什麼了,總結爲一句話:AQS使用一個Volatile的int類型的成員變量來表示同步狀態,通過內置的FIFO隊列來完成資源獲取的排隊工作,通過CAS完成對State值的修改。
然後再來一張圖,使得理解更加深刻:
圖片來源:Java技術之AQS詳解
好了,AQS暫時可以先了解到這裏了,知道這些後,在後面瞭解ReentrantLock時就會變的容易些,並且後面通過源碼學習ReentrantLock時,由於會使用到AQS的模版方法,所以也會講解到AQS的內容。
劍指ReentrantLock源碼:
2.0、ReentrantLock vs Synchronized
在瞭解ReentrantLock之前,先將ReentrantLock與Synchronized進行比較下,這樣可以更加了解ReentrantLock的特性,也有助於下面源碼的閱讀;
2.1、ReentrantLock的公平鎖與非公平鎖
創建一個ReentrantLock對象,在創建對象時,如果不指定公平鎖的話,默認是非公平鎖;
①、簡單瞭解下什麼是公平鎖,什麼是非公平鎖?
公平鎖:按照申請同步鎖的順序來獲取鎖;
非公平鎖:不會按照申請鎖的順序獲取鎖,存在鎖的搶佔;
注:後面會通過源碼瞭解下非公平鎖和公平鎖是怎樣獲取鎖的。
②、源碼如下:
// 默認是非公平的鎖
ReentrantLock lock = new ReentrantLock();
// 構造方法默認創建了一個 NonfairSync 非公平鎖對象
public ReentrantLock() {
// NonfairSync繼承了Sync類,Sync類又繼承了AQS類
sync = new NonfairSync();
}
// 傳入參數 true,指定爲公平鎖
ReentrantLock lock = new ReentrantLock(true);
// 傳入參數的構造方法,當fair爲true時,創建一個公平鎖對象,否則創建一個非公平鎖對象
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
2.2、通過源碼看下非公平鎖的加鎖機制:(獨佔模式)
①、開始先通過一個簡單流程圖來看下獨佔模式下加鎖的流程:
圖片來源:美團技術團隊
②、源碼分析:加鎖時首先使用CAS算法嘗試將state狀態變量設置爲1,設置成功後,表示當前線程獲取到了鎖,然後將獨佔鎖的擁有者設置爲當前線程;如果CAS設置不成功,則進入Acquire方法進行後續處理。
final void lock() {
// 使用CAS算法嘗試將state狀態變量設置爲1
if (compareAndSetState(0, 1))
// 設置成功後,表示當前線程獲取到了鎖,然後將獨佔鎖的擁有者設置爲當前線程
setExclusiveOwnerThread(Thread.currentThread());
else
// 進行後續處理,會涉及到重入性、創建Node節點加入到隊列尾等
acquire(1);
}
③、探究下acquire(1) 方法裏面是什麼呢 acquire(1) 方法是AQS提供的方法:
public final void acquire(int arg) {
/**
* 使用tryAcquire()方法,讓當前線程嘗試獲取同步鎖,獲取成的話,就不會執行後面的acquireQueued()
* 方法了,這是由於 && 邏輯運算符的特性決定的。
*
* 如果使用tryAcquire()方法獲取同步鎖失敗的話,就會繼續執行acquireQueued()方法,它的作用是
* 一直死循環遍歷同步隊列,直到使addWaiter()方法創建的節點中線程獲取到鎖。
*
* 如果acquireQueued()返回的true,這個true不是代表成功的獲取到鎖,而是代表當前線程是否存在
* 中斷標誌,如果存在的話,在獲取到同步鎖後,需要使用selfInterrupt()對當前線程進行中斷。
*/
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
1)、tryAcquire(arg) 方法源碼解讀:NonfairSync 非公平鎖中重寫了AQS的tryAcquire()方法
final boolean nonfairTryAcquire(int acquires) {
// 當前線程
final Thread current = Thread.currentThread();
// 獲取當前state同步狀態變量值,由於使用volatile修飾,單獨的讀寫操作具有原子性
int c = getState();
// 如果狀態值爲0
if (c == 0) {
// 使用compareAndSetState方法這個CAS算法嘗試將state同步狀態變量設置爲1 獲取同步鎖
if (compareAndSetState(0, acquires)) {
// 然後將獨佔鎖的擁有者設置爲當前線程
setExclusiveOwnerThread(current);
return true;
}
}
// 如果擁有獨佔鎖的的線程是當前線程的話,表示當前線程需要重複獲取鎖(重入鎖)
else if (current == getExclusiveOwnerThread()) {
// 當前同步狀態state變量值加1
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
// 寫入state同步狀態變量值,由於使用volatile修飾,單獨的讀寫操作具有原子性
setState(nextc);
return true;
}
return false;
}
2)、addWaiter( Node.EXCLUSIVE ) :創建一個同步隊列Node節點,同時綁定節點的模式爲獨佔模式,並且將創建的節點插入到同步隊列尾部;addWaiter( ) 方法是AQS提供方法。
private Node addWaiter(Node mode) {
// model參數是獨佔模式,默認爲null;
Node node = new Node(Thread.currentThread(), mode);
// 將當前同步隊列的tail尾節點的地址引用賦值給pre變量
Node pred = tail;
// 如果pre不爲null,說明同步隊列中存在節點
if (pred != null) {
// 當前節點的前驅結點指向pre尾節點
node.prev = pred;
// 使用CAS算法將當前節點設置爲尾節點,使用CAS保證其原子性
if (compareAndSetTail(pred, node)) {
// 尾節點設置成功,將pre舊尾節點的後繼結點指向新尾節點node
pred.next = node;
return node;
}
}
// 如果尾節點爲null,表示同步隊列中還沒有節點,enq()方法將當前node節點插入到隊列中
enq(node);
return node;
}
3)、說完addWaiter( Node.EXCLUSIVE )方法,接下來說下**acquireQueued()**方法,它是怎樣使addWaiter()創建的節點中的線程獲取到state同步鎖的。(這個方法也是AQS提供的)
源碼走起:
final boolean acquireQueued(final Node node, int arg) {
// 標誌cancelAcquire()方法是否執行
boolean failed = true;
try {
// 標誌是否中斷,默認爲false不中斷
boolean interrupted = false;
for (;;) {
// 獲取當前節點的前驅結點
final Node p = node.predecessor();
/**
* 如果當前節點的前驅結點已經是同步隊列的頭結點了,說明了兩點內容:
* 1、其前驅結點已經獲取到了同步鎖了,並且鎖還沒釋放
* 2、其前驅結點已經獲取到了同步鎖了,但是鎖已經釋放了
*
* 然後使用tryAcquire()方法去嘗試獲取同步鎖,如果前驅結點已經釋放了鎖,那麼就會獲取成功,
* 否則同步鎖獲取失敗,繼續循環
*/
if (p == head && tryAcquire(arg)) {
// 將當前節點設置爲同步隊列的head頭結點
setHead(node);
// 然後將當前節點的前驅結點的後繼結點置爲null,幫助進行垃圾回收
p.next = null; // help GC
failed = false;
// 返回中斷的標誌
return interrupted;
}
/**
* shouldParkAfterFailedAcquire()是對當前節點的前驅結點的狀態進行判斷,以及去針對各種
* 狀態做出相應處理,由於文章篇幅問題,具體源碼本文不做講解;只需知道如果前驅結點p的狀態爲
* SIGNAL的話,就返回true。
*
* parkAndCheckInterrupt()方法會使當前線程進去waiting狀態,並且查看當前線程是否被中斷,
* interrupted() 同時會將中斷標誌清除。
*/
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
// 中斷標誌置爲true
interrupted = true;
}
} finally {
if (failed)
/**
* 如果for(;;)循環中出現異常,並且failed=false沒有執行的話,cancelAcquire方法
* 就會將當前線程的狀態置爲 node.CANCELLED 已取消狀態,並且將當前節點node移出
* 同步隊列。
*/
cancelAcquire(node);
}
}
4)、最後說下 selfInterrupt() 方法, 這個方法就是將當前線程進行中斷:
static void selfInterrupt() {
// 中斷當前線程
Thread.currentThread().interrupt();
}
2.3、公平鎖與非公平鎖在加鎖時的區別:
①、公平鎖 FairSync 的加鎖 lock() 加鎖方法:
final void lock() {
acquire(1);
}
②、非公平鎖 NonfairSync 的加鎖 lock() 加鎖方法:上面講解源碼的時候有提到喲,還有印象嗎,沒印象的話也沒關係,不要哭 , 嘿嘿,我都準備好了。 源碼奉上:
final void lock() {
/**
* 看到這,是不是發現了什麼,非公平鎖在此處直觀看的話,發現比公平鎖多了這幾行代碼;
* 這裏就是使得線程存在了一個搶佔,如果當前同步隊列中的head頭結點中 線程A 剛好釋放了同步鎖,
* 然後此時 線程B 正好來了,那麼此時線程B就會獲取到鎖,而此時同步隊列中head頭結點的後繼結點中的
* 線程C 就無法獲取到同步鎖,只能等待線程B釋放鎖後,嘗試獲取鎖了。
*/
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
③、除了上面那處不同之外,還有別的地方嗎;別急,再看看 acquire(1) 方法是否一樣呢?
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
誒呀,方法點進去都是一樣的呀,可不嘛,都是調用的AQS提供的 acquire(1) 方法;但是彆着急,上面在講解非公平鎖加鎖時,有提到的 tryAcquire(arg) 方法在AQS的不同子孫類中都有各自的實現的。現在打開公平鎖的 tryAcquire(arg) 方法看看其源碼與非公平鎖有什麼區別:
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
/**
* 通過對比源碼發現,公平鎖比非公平鎖多了這塊代碼: !hasQueuedPredecessors()
* hasQueuedPredecessors() 是做什麼呢?就是判斷當前同步隊列中是否存在節點,如果存在節點呢,
* 就返回true,由於前面有個 !,那麼就是false,再根據 && 邏輯運算符的特性,不會繼續執行了;
*
* tryAcquire()方法直接返回false,後面的邏輯就和非公平鎖的一致了,就是創建Node節點,並將
* 節點加入到同步隊列尾; 公平鎖:發現當前同步隊列中存在節點,有線程在自己前面已經申請可鎖,那
* 自己就得乖乖的向後面排隊去。
*
* 友情提示:在生活中,我們也需要按照先來後到去排隊,保證素質; 還有就是怕你們不排隊被別人打了。
*/
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;
}
鬆口氣,從中午一直寫到下午快四點了,先讓我歇口氣,快累成狗了;本文還剩下釋放鎖部分沒寫呢,歇口氣,喝口水繼續。
注意:ReentrantLock在釋放鎖的時候,並不區分公平鎖和非公平鎖。
2.4、通過源碼看下釋放鎖機制:(獨佔模式)
①、unlock() 釋放鎖的方法:
public void unlock() {
// 釋放鎖時,需要將state同步狀態變量值進行減 1,傳入參數 1
sync.release(1);
}
②、release( int arg ) 方法解析:(此方法是AQS提供的)
public final boolean release(int arg) {
// tryRelease方法:嘗試釋放鎖,成功true,失敗false
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
// 頭結點不爲空並且頭結點的waitStatus不是初始化節點情況,然後喚醒此阻塞的線程
unparkSuccessor(h);
return true;
}
return false;
}
注意:這裏的判斷條件爲什麼是h != null && h.waitStatus != 0?
h == null Head還沒初始化。初始情況下,head == null,第一個節點入隊,Head會被初始化一個虛擬節點。所以說,這裏如果還沒來得及入隊,就會出現head == null 的情況。
h != null && waitStatus == 0 表明後繼節點對應的線程仍在運行中,不需要喚醒。
h != null && waitStatus < 0 表明後繼節點可能被阻塞了,需要喚醒。
③、然後再來看看tryRelease(arg) 方法:
protected final boolean tryRelease(int releases) {
// 當前state狀態值進行減一
int c = getState() - releases;
// 如果當前獨佔鎖的擁有者不是當前線程,則拋出 非法監視器狀態 異常
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
// 更新state同步狀態值
setState(c);
return free;
}
④、最後看看unparkSuccessor(Node node) 方法:
private void unparkSuccessor(Node node) {
// 獲取頭結點waitStatus
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
// 獲取當前節點的下一個節點
Node s = node.next;
// 如果下個節點是null或者下個節點被cancelled,就找到隊列最開始的非cancelled狀態的節點
if (s == null || s.waitStatus > 0) {
s = null;
// 就從尾部節點開始找,到隊首,找到隊列第一個waitStatus<0的節點。
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
// 如果當前節點的後繼結點不爲null,則將其節點中處於阻塞狀態的線程unpark喚醒
if (s != null)
LockSupport.unpark(s.thread);
}
注意:爲什麼要從後往前找第一個非Cancelled的節點呢?原因如下:
由於之前加鎖時的**addWaiter( )**方法的原因;
private Node addWaiter(Node mode) {
// model參數是獨佔模式,默認爲null;
Node node = new Node(Thread.currentThread(), mode);
// 將當前同步隊列的tail尾節點的地址引用賦值給pre變量
Node pred = tail;
// 如果pre不爲null,說明同步隊列中存在節點
if (pred != null) {
// 當前節點的前驅結點指向pre尾節點
node.prev = pred;
// 使用CAS算法將當前節點設置爲尾節點,使用CAS保證其原子性
if (compareAndSetTail(pred, node)) {
// 尾節點設置成功,將pre舊尾節點的後繼結點指向新尾節點node
pred.next = node;
return node;
}
}
// 如果尾節點爲null,表示同步隊列中還沒有節點,enq()方法將當前node節點插入到隊列中
enq(node);
return node;
}
從這裏可以看到,節點入隊並不是原子操作,也就是說,node.prev = pred ; compareAndSetTail( pred, node ) 這兩個地方可以看作Tail入隊的原子操作,但是此時 pred.next = node; 還沒執行,如果這個時候執行了unparkSuccessor方法,就沒辦法從前往後找了,所以需要從後往前找。還有一點原因,在產生CANCELLED狀態節點的時候,先斷開的是Next指針,Prev指針並未斷開,因此也是必須要從後往前遍歷才能夠遍歷完全部的Node。
end! 長吸一口氣,終於本文算是寫完了,最後再看看有沒有錯別字,以及排排版。
後續還會出一篇結合CountDownLatch源碼學習共享鎖(共享模式)的文章。
謝謝大家閱讀,鑑於本人水平有限,如有問題敬請提出。 |