JDK 中獨佔鎖(排他鎖)的實現除了使用關鍵字 synchronized 外,還可以使用ReentrantLock。雖然在性能上 ReentrantLock 和 synchronized 沒有什麼大區別,但 ReentrantLock 相比 synchronized 而言功能更加豐富,使用起來更爲靈活,也更適合複雜的併發場景。
ReentrantLock 常常對比着 synchronized 來分析,我們先對比着來看然後再一點一點分析。
- synchronized 是獨佔鎖,加鎖和解鎖的過程自動進行,易於操作,但不夠靈活。ReentrantLock 也是獨佔鎖,加鎖和解鎖的過程需要手動進行,不易操作,但非常靈活。
- synchronized 可重入,因爲加鎖和解鎖自動進行,不必擔心最後是否釋放鎖;ReentrantLock 也可重入,但加鎖和解鎖需要手動進行,且次數需一樣,否則其他線程無法獲得鎖。
- synchronized 不可響應中斷,一個線程獲取不到鎖就一直等着;ReentrantLock 可以相應中斷。
ReentrantLock 好像比 synchronized 關鍵字沒好太多,我們再去看看 synchronized 所沒有的。一個最主要的就是 ReentrantLock 還可以實現公平鎖機制。什麼叫公平鎖呢?也就是在鎖上等待時間最長的線程將獲得鎖的使用權。通俗的理解就是誰排隊時間最長誰先執行獲取鎖
。
在講解 ReentrantLock 之前,必須先要了解一個叫 AQS(隊列同步器)的東西,這個東西也是併發包的,AQS 就是AbstractQueuedSynchronizer
,它是 Java 併發包中的一個核心,ReentrantLock 以及ReentrantReadWriteLock
都是基於 AQS 實現的。想要搞明白 ReentrantLock 是如何實現的,就必須先學習AbstractQueuedSynchronizer
。本文結合 JDK1.8 的源碼先剖析下隊列同步器,然後再詳解 ReentrantLock 中如何應用隊列同步器實現獨佔鎖的。
隊列同步器(AQS)
隊列同步器 AbstractQueuedSynchronizer,是用來構建鎖或者其他同步組件的基礎框架,它使用了一個 volatile 修飾的 int 成員變量表示同步狀態,通過內置的 FIFO 隊列來完成資源獲取線程的排隊工作。
同步器的主要使用方式是繼承,子類通過繼承同步器並實現它的抽象方法來管理同步狀態,在抽象方法的實現過程中免不了要對同步狀態進行更改,這時就需要同步器提供的 3 個方法getState()
、setState(int newState)
和compareAndSetState(int expect, int update)
來進行操作,因爲它們能夠保證狀態的改變是安全的。子類推薦被定義爲自定義同步組件的靜態內部類,同步器自身沒有實現任何同步接口,它僅僅是定義了若干同步狀態獲取和釋放方法來供自定義同步組件使用,同步器即可以支持獨佔式獲取同步狀態,也可以支持共享式地獲取同步狀態,這樣方便實現不同類型的同步組件(ReentrantLock
、ReentrantReadWriteLock
、CountDownLatch
等)。
AQS 是實現鎖(也可以是任何同步組件)的關鍵:在鎖中聚合同步器,利用同步器實現鎖的語義。兩者的關係:鎖是面向使用者的,他定義了使用者與鎖交互的接口(比如允許兩個線程並行訪問),隱藏了實現細節;同步器是面向鎖的實現者,它簡化了鎖的實現方式,屏蔽了同步管理狀態、線程的排隊、等待與喚醒等底層操作。
隊列同步器的接口及模板方法
同步器基於模板設計模式實現的,使用者需要繼承同步器並重寫指定的方法,隨後將同步器組合在自定義的同步組件的實現中,並調用同步器提供的模板方法,而這些模板方法會調用使用者重寫的方法。
重寫同步器指定方法時需要使用同步器提供的如下三個方法來訪問或修改同步狀態:
-
getState()
:獲取當前同步狀態 -
setState(int new State)
:設置當前同步狀態 -
compareAndState(int expect,int update)
:使用 CAS 設置當前狀態,該方法能夠保證狀態設置的原子性。
同步器可重寫的方法:
實現自定義同步組件時,將會調用AbstractQueuedSynchronizer
自身已經寫好的模板方法,這些模板方法與描述:
同步器可重寫的方法
注:模板方法基本分爲三類:獨佔式同步狀態獲取與釋放、共享式同步狀態獲取與釋放和查詢同步隊列中等待線程情況。
AbstractQueuedSynchronizer
要求子類必須覆寫的方法如下,之所以要求子類重寫這些方法,是爲了讓使用者可以在其中加入自己的判斷邏輯。
同步器提供的模版方法
AQS 中提供了很多關於鎖的實現方法
-
getState()
:獲取鎖的標誌 state 值 -
setState()
:設置鎖的標誌 state 值 -
tryAcquire(int)
:獨佔方式獲取鎖。嘗試獲取資源,成功則返回 true,失敗則返回 false -
tryRelease(int)
:獨佔方式釋放鎖。嘗試釋放資源,成功則返回 true,失敗則返回 false
隊列同步器數據結構
同步器依賴於內部的同步隊列(一個FIFO
雙向隊列)來完成同步狀態的管理,當前線程獲取同步狀態失敗時,同步器會將當前線程以及等待狀態等信息構成一個節點(Node)並將其加入同步隊列,同時阻塞當前線程,當同步狀態釋放時,會將首節點中的線程喚醒,使其再次嘗試獲取同步狀態。
在AbstractQueuedSynchronizer
類中,Node 類是靜態內部類,其源碼如下:
static final class Node {
/** Marker to indicate a node is waiting in shared mode */
static final Node SHARED = new Node();
/** Marker to indicate a node is waiting in exclusive mode */
static final Node EXCLUSIVE = null;
/** waitStatus value to indicate thread has cancelled */
static final int CANCELLED = 1;
/** waitStatus value to indicate successor's thread needs unparking */
static final int SIGNAL = -1;
/** waitStatus value to indicate thread is waiting on condition */
static final int CONDITION = -2;
/**
* waitStatus value to indicate the next acquireShared should
* unconditionally propagate
*/
static final int PROPAGATE = -3;
//等待狀態
volatile int waitStatus;
//指向前一個結點的指針
volatile Node prev;
//指向後一個結點的指針
volatile Node next;
//當前結點代表的線程
volatile Thread thread;
//等待隊列中的後繼節點,如果當前節點是共享的,那麼這個字段將是SHARED常量,即節點類型(獨佔和共享)和等待隊列中的後繼節點共用同一個字段
Node nextWaiter;
/**
* Returns true if node is waiting in shared mode.
*/
final boolean isShared() {
return nextWaiter == SHARED;
}
final Node predecessor() throws NullPointerException {
Node p = prev;
if (p == null)
throw new NullPointerException();
else
return p;
}
Node() { // Used to establish initial head or SHARED marker
}
Node(Thread thread, Node mode) { // Used by addWaiter
this.nextWaiter = mode;
this.thread = thread;
}
Node(Thread thread, int waitStatus) { // Used by Condition
this.waitStatus = waitStatus;
this.thread = thread;
}
}
同時,在AbstractQueuedSynchronizer
類中,又單獨定義了隊列頭結點、尾結點、同步狀態變量。
//指向隊列頭結點
private transient volatile Node head;
//指向隊列尾結點
private transient volatile Node tail;
//同步狀態變量
private volatile int state;
protected final int getState() {
return state;
}
protected final void setState(int newState) {
state = newState;
}
protected final boolean compareAndSetState(int expect, int update) {
// See below for intrinsics setup to support this
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
同步隊列的結構如圖:
同步隊列的結構
爲了接下來能夠更好的理解加鎖和解鎖過程的源碼,對該同步隊列的特性進行簡單的說明:
- 同步隊列是個先進先出(FIFO)隊列,獲取鎖失敗的線程將構造結點並加入隊列的尾部,加入隊列的過程必須保證線程安全,爲什麼必須保證線程安全?因爲要面對同時有多條線程沒有獲取到同步狀態要加入同步隊列尾部的情況;
- 隊列首結點是獲取同步狀態成功的線程節點;
- 前驅結點線程釋放鎖後將嘗試喚醒後繼結點中處於阻塞狀態的線程
同步器將節點加入同步隊列的過程:
同步器將節點加入同步隊列的過程
同步隊列遵循 FIFO,首節點是獲取同步狀態成功的節點,首節點的線程在釋放同步狀態時,將會喚醒後繼節點,而後繼節點將會在獲取同步狀態成功時將自己設置爲新的首節點。
同步隊列遵循 FIFO
注:設置首節點是通過由已經獲取到了同步狀態的線程來完成的,由於只有一個線程能夠獲取到同步狀態,因此設置頭節點的方法並不需要 CAS 來保障,它只需要讓head 指針指向原首節點的後繼節點並斷開原首節點的 next 引用即可。
隊列同步器提供的獨佔式同步狀態獲取方法
通過AbstractQueuedSynchronizer
類提供的acquire(int arg)
方法可以獲取同步狀態,該方法對中斷不敏感,也就是說由於線程獲取同步狀態失敗後進入同步隊列中,後繼對線程進行中斷操作時,線程不會從同步隊列移除。acquire 方法:
public final void acquire(int arg) {
//tryAcquie()方法具體要交給子類去實現,AbstractQueuedSynchronizer類中不實現
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
上述代碼中完成了同步狀態的獲取、節點構造、加入同步隊列以及同步隊列中自旋等待的相關工作。
首先調用自定義同步器(AbstractQueuedSynchronizer的子類)
實現的tryAcquire(int arg)
方法,該方法保證線程安全的獲取同步狀態,如果同步狀態獲取失敗,則構造同步節點(獨佔式Node.EXCLUSIVE
,同一時刻只能有一個線程成功獲取同步狀態)並通過addWaiter(Node node)
方法將該節點加入到同步隊列的尾部,最後調用acquireQueued(Node node,int arg)
方法,使得該節點以“死循環”的方式獲取同步狀態。如果獲取不到就阻塞節點中的線程,而被阻塞線程的喚醒主要依靠前驅節點的出隊或阻塞線程被中斷來實現。
節點的構造以及加入同步隊列依靠於 addWaiter 和 enq 方法:
private Node addWaiter(Node mode) {
首先創建一個新節點,並將當前線程實例封裝在內部,mode這裏爲null
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
Node pred = tail;
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
///入隊的邏輯
enq(node);
return node;
}
/**
* 隊列不空時向尾部添加結點的邏輯在enq(node)方法中也有,之所以會有這部分“重複代碼”是對某些特殊情況進行提前處理,犧牲一定的代碼可讀性換取性能提升。
*/
private Node enq(final Node node) {
for (;;) {
//t指向當前隊列的最後一個節點,隊列爲空則爲null
Node t = tail;
//隊列爲空
if (t == null) {
//此時鏈表沒有節點,需要初始化讓head跟tail都指向一個哨兵節點
//構造新結點,CAS方式設置爲隊列首元素,當head==null時更新成功
if (compareAndSetHead(new Node()))
tail = head;//尾指針指向首結點
} else { //隊列不爲空
node.prev = t;
if (compareAndSetTail(t, node)) { //CAS將尾指針指向當前結點,當t(原來的尾指針)==tail(當前真實的尾指針)時執行成功
t.next = node; //原尾結點的next指針指向當前結點
return t;
}
}
}
}
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
//死循環,正常情況下線程只有獲得鎖才能跳出循環
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
//將當前結點設置爲隊列頭結點
setHead(node);
p.next = null; // help GC
failed = false;
//正常情況下死循環唯一的出口
return interrupted;
}
//判斷是否要阻塞當前線程
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
看完enq(final Node node)
方法後,發現事實上 AQS 隊列的頭節點其實是個哨兵節點。
在enq(final Node node)
中,同步器通過死循環的方式來確保節點的添加,在死循環中只有通過 CAS 將當前節點設置爲尾節點之後,當前線程才能從該方法返回,否則的話當前線程不斷地嘗試設置。enq(final Node node)
方法將併發添加節點的請求通過 CAS 變得“串行化”了。循環加 CAS 操作是實現樂觀鎖的標準方式,CAS 是爲了實現原子操作而出現的,所謂的原子操作指操作執行期間,不會受其他線程的干擾。Java 實現的 CAS 是調用 unsafe 類提供的方法,底層是調用 C++ 方法,直接操作內存,在 CPU 層面加鎖,直接對內存進行操作。
來看shouldParkAfterFailedAcquire(Node pred, Node node)
,從方法名上我們可以大概猜出這是判斷是否要阻塞當前線程的,源碼如下:
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
if (ws == Node.SIGNAL) //狀態爲SIGNAL
/*
* This node has already set status asking a release
* to signal it, so it can safely park.
*/
return true;
if (ws > 0) { //狀態爲CANCELLED,
/*
* Predecessor was cancelled. Skip over predecessors and
* indicate retry.
*/
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else { //狀態爲初始化狀態(ReentrentLock語境下)
/*
* 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;
}
可以看到針對前驅結點 pred 的狀態會進行不同的處理:
- pred 狀態爲 SIGNAL,則返回 true,表示要阻塞當前線程;
- pred 狀態爲 CANCELLED,則一直往隊列頭部回溯直到找到一個狀態不爲CANCELLED 的結點,將當前節點 node 掛在這個結點的後面;
- pred 的狀態爲初始化狀態,此時通過 CAS 操作將 pred 的狀態改爲 SIGNAL
其實這個方法的含義很簡單,就是確保當前結點的前驅結點的狀態爲 SIGNAL,SIGNAL 意味着線程釋放鎖後會喚醒後面阻塞的線程。畢竟,只有確保能夠被喚醒,當前線程才能放心的阻塞。
要注意只有在前驅結點已經是 SIGNAL 狀態後纔會執行後面的方法立即阻塞,對應上面的第一種情況。其他兩種情況則因爲返回 false 而重新執行一遍
acquireQueued()
方法的源碼錶明節點在進入隊列後,就進入了一個自旋狀態,每個節點(或者說每個線程),都在自省觀察,當條件滿足,獲取到同步狀態,就可以從這個自旋過程中退出,否則依舊留在自旋過程中,
這個過程如下圖所示。
自旋狀態
在acquireQueued(final Node node, int arg)
方法中,線程在“死循環”中嘗試獲取同步狀態,而只有前驅節點是頭節點時才能夠嘗試獲取同步狀態,原因如下:
- 頭節點是成功獲取到同步狀態的節點,而頭節點線程獲取到同步狀態後,將會喚醒其後繼節點,後繼節點的線程在被喚醒後需要檢查自己的前驅節點是否是頭節點;
- 維護同步隊列的 FIFO 原則
可以看到節點與及節點之間在循環檢查的過程中基本上不相互通信,而是簡單地判斷自己的前驅是否爲頭節點,這樣就使得節點的釋放符合 FIFO,並且對於方便對過早通知進行處理。
獨佔式同步狀態獲取流程如下圖,也就是acquire(int arg)
方法的執行流程:
獨佔式同步狀態獲取流程
當同步狀態獲取成功,當前線程從acquire(int arg)
方法返回,這也就代表着當前線程獲得了鎖。
隊列同步器提供的獨佔式同步狀態釋放方法
釋放同步狀態,通過調用同步器的release(int arg)
方法可以釋放同步狀態,該方法在釋放了同步狀態後,會喚醒其後繼節點(進而使後繼節點重新嘗試獲取同步狀態)。解鎖的源碼相對簡單,代碼如下:
public final boolean release(int arg) {
//tryRelease()方法具體要交給子類去實現,AbstractQueuedSynchronizer類中不實現
if (tryRelease(arg)) {
Node h = head;
//當前隊列不爲空且頭結點狀態不爲初始化狀態(0)
if (h != null && h.waitStatus != 0)
unparkSuccessor(h); //喚醒同步隊列中被阻塞的線程
return true;
}
return false;
}
若當前線程已經完全釋放鎖,即鎖可被其他線程使用,則還應該喚醒後續等待線程。不過在此之前需要進行兩個條件的判斷:
-
h != null
是爲了防止隊列爲空,即沒有任何線程處於等待隊列中,那麼也就不需要進行喚醒的操作; -
h.waitStatus != 0
是爲了防止隊列中雖有線程,但該線程還未阻塞,由前面的分析知,線程在阻塞自己前必須設置其前驅結點的狀態爲 SIGNAL,否則它不會阻塞自己;
該方法執行時,會喚醒頭節點的後繼節點線程,unparkSuccerssor(Node node)
方法使用 LcokSupport 來喚醒處於等待(阻塞)狀態的線程。
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
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);
}
一般情況下只要喚醒後繼結點的線程就行了,但是後繼結點可能已經取消等待,所以從隊列尾部往前回溯,找到離頭結點最近的正常結點,並喚醒其線程。
獨佔式同步狀態釋放流程如下圖,也就是release(int arg)
方法的執行流程:
獨佔式同步狀態釋放流程
獨佔式同步狀態獲取和釋放:
- 在獲取同步狀態時,同步器會維持一個同步隊列,獲取失敗的線程都會被加入到同步隊列中,並在同步隊列中自旋(判斷自己前驅節點爲頭節點)。
- 移出隊列(停止自旋)的條件是前驅節點爲頭節點且成功獲取了同步狀態。在釋放同步狀態時,同步器調用
tryRelease(int arg)
方法釋放同步狀態,然後喚醒頭節點的後繼節點。
共享式同步狀態獲取與釋放
共享式獲取與獨佔式獲取的區別:同一時刻能否有多個線程同時獲取到同步狀態。
以文件的讀寫爲例:
- 如果有一個程序在讀文件,那麼這一時刻的寫操作均被阻塞,而讀操作能夠同時進行。
- 如果有一個程序在寫文件,那麼這一時刻不管是其他的寫操作還是讀操作,均被阻塞。
- 寫操作要求對資源的獨佔式訪問,而讀操作可以是共享式訪問。
調用同步器的acquireShared()
模板方法,可以實現共享式獲取同步狀態。
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
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;
}
}
//用LockSupport的park方法把當前線程阻塞住
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
- 當前線程首先調用
tryAcquireShared()
這個被子類重寫的方法,共享式的獲取同步狀態。如果返回值大於等於 0,表示獲取成功並返回。 - 如果返回值小於0表示獲取失敗,調用 doAcquireShared() 方法,讓線程進入自旋狀態。
- 自旋過程中,如果當前節點的前驅節點是頭結點,且調用
tryAcquireShared()
方法返回值大於等於 0,則退出自旋。否則,繼續進行自旋。
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
首先去嘗試釋放資源tryReleaseShared(arg)
,如果釋放成功了,就代表有資源空閒出來,那麼就用 doReleaseShared() 去喚醒後續結點。
ReentrantLock 對隊列同步器的應用
syncronized 關鍵字隱式的支持重進入,比如 syncronized 修飾一個遞歸方法,在方法執行時,執行線程在獲取了鎖之後仍能連續多次地獲取該鎖。
ReentrantLock 雖然沒能像 synchronized 關鍵字一樣支持隱式的重進入,但是在調用 lock() 方法時,已經獲取了鎖的線程,能夠再次調用 lock() 方法而不被阻塞。要實現可重入的特性,就要解決以下兩個問題:
- 線程再次獲取鎖,鎖需要去識別獲取鎖的線程是否爲當前佔據鎖的線程,如果是,則再次成功獲取
- 鎖的最終釋放,線程重複 n 次獲取了鎖,隨後在第 n 次釋放該鎖後,其他線程能夠獲取到鎖。鎖的最終釋放要求鎖對於獲取進行自增,對於釋放進行自減,當計數等於0時表示鎖已經成功釋放。
ReentrantLock 通過組合自定義隊列同步器來實現鎖的可重入式獲取與釋放。
ReentrantLock 的類圖如下,可以看出 ReentrantLock 實現 Lock 接口,Sync 與 ReentrantLock 是組合關係,且 FairSync(公平鎖)、NonfairySync(非公平鎖)是 Sync 的子類。
FairSync、NonfairySync、Sync
公平鎖是 FairSync,非公平鎖是 NonfairSync。而不管是 FairSync 還是 NonfariSync,都間接繼承自 AbstractQueuedSynchronizer 這個抽象類,如下圖所示
NonfariSync 的類圖
ReentrantLock 非公平模式獲取及釋放鎖
ReentrantLock 非公平模式下的獲取鎖的代碼如下:
//實現Lock接口的lock方法,調用本方法當前線程獲取鎖,拿鎖成功後就返回
public void lock() {
//非公平模式下,sync指向的對象類型是NonfairSync
sync.lock();
}
//實現Lock接口的tryLock方法,嘗試非阻塞的獲取鎖,調用本方法後立刻返回,如果能獲取到鎖則返回true,否則返回false
public boolean tryLock() {
return sync.nonfairTryAcquire(1);
}
靜態內部類 NonfairSync 的源碼如下:
static final class NonfairSync extends Sync {
private static final long serialVersionUID = 7316153563782823691L;
/**
* Performs lock. Try immediate barge, backing up to normal
* acquire on failure.
*/
final void lock() {
//首先CAS嘗試下獲取鎖,先假設每次lock都是非重入
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
//這裏調用的是父類AbstractQueuedSynchronizer的acquire()方法,
//而acquire()方法中又要調用交由子類去實現的tryAcquiretryAcquire()方法
//所以會調到下面NonfairSync類的tryAcquire()方法
acquire(1);
}
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
nonfairTryAcquire()
方法的源碼如下:
//本方法寫在Sync類中,而不是FairSync類中
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
//如果當前鎖閒置,就CAS嘗試下獲取鎖
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
nonfairTryAcquire()
方法增加了再次獲取同步狀態的處理邏輯:通過判斷當前線程是否爲已經獲取了鎖的線程來決定獲取操作是否成功,如果是則將同步狀態值增加並返回 true,表示獲取同步狀態成功。
ReentrantLock 類的 unlock 方法:
public void unlock() {
//這裏調用的是父類AbstractQueuedSynchronizer的release()方法,
//而release()方法中又要調用交由子類去實現的tryRelease()方法
//所以會調到Sync類的tryRelease()方法
sync.release(1);
}
由於公平鎖與非公平鎖的差異主要體現在獲取鎖上,因此 tryAcquire() 方法由NonfairSync 類與 FairSync 類分別去實現,而無論是公平鎖還是非公平鎖,鎖的釋放過程都是一樣的,因此 tryRelease() 方法由 Sync 類來實現。源碼如下:
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
如果該鎖被獲取了 N 次,那麼前 (N-1) 次tryRelease(int releases)
方法必須返回 false,而只有同步狀態完全釋放了,才能返回 true。
ReentrantLock 公平模式獲取及釋放鎖
公平鎖模式下,對鎖的獲取有嚴格的條件限制。在同步隊列有線程等待的情況下,所有線程在獲取鎖前必須先加入同步隊列,隊列中的線程按加入隊列的先後次序獲得鎖。
static final class FairSync extends Sync {
private static final long serialVersionUID = -3000897897090466540L;
final void lock() {
//這裏調用的是父類AbstractQueuedSynchronizer的acquire()方法
//父類的acquire()方法中會調用tryAcquire()方法,該方法由子類去實現
//即最終會調用到FairSync實現的tryAcquire()方法
acquire(1);
}
/**
* Fair version of tryAcquire. Don't grant access unless
* recursive call or no waiters or is first.
*/
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
//在真正CAS獲取鎖之前加了hasQueuedPredecessors()方法
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;
}
}
hasQueuedPredecessors()
方法在AbstractQueuedSynchronizer
類中(模板方法模式的典型應用,所有公共的方法全部都由AbstractQueuedSynchronizer
寫好,只有個性化邏輯才下沉給子類去實現),源碼如下:
public final boolean hasQueuedPredecessors() {
// The correctness of this depends on head being initialized
// before tail and on head.next being accurate if the current
// thread is first in queue.
Node t = tail; // Read fields in reverse initialization order
Node h = head;
Node s;
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
從方法名我們就可知道這是判斷隊列中是否有優先級更高的等待線程,隊列中哪個線程優先級最高?由於頭結點是當前獲取鎖的線程,隊列中的第二個結點代表的線程優先級最高。那麼只要判斷隊列中第二個結點是否存在以及這個結點是否代表當前線程就行了。這裏分了兩種情況進行探討:
第二個結點已經完全插入,但是這個結點是否就是當前線程所在結點還未知,所以通過
s.thread != Thread.currentThread()
進行判斷,如果爲 true,說明第二個結點代表其他線程。第二個結點並未完全插入,我們知道結點入隊一共分三步:
- 待插入結點的 pre 指針指向原尾結點;
- CAS 更新尾指針;
- 原尾結點的 next 指針指向新插入結點。所以
(s = h.next) == null
就是用來判斷 2 剛執行成功但還未執行 3 這種情況的。這種情況第二個結點必然屬於其他線程。
當前有優先級更高的線程在隊列中等待時,那麼當前線程將不會執行 CAS 操作去獲取鎖,保證了線程獲取鎖的順序與加入同步隊列的順序一致,很好的保證了公平性,但也增加了獲取鎖的成本。
基於 FIFO 的同步隊列是怎樣實現非公平搶佔鎖的
由 FIFO 隊列的特性知,先加入同步隊列等待的線程會比後加入的線程更靠近隊列的頭部,那麼它將比後者更早的被喚醒,它也就能更早的得到鎖。從這個意義上,對於在同步隊列中等待的線程而言,它們獲得鎖的順序和加入同步隊列的順序一致,這顯然是一種公平模式。然而,線程並非只有在加入隊列後纔有機會獲得鎖,哪怕同步隊列中已有線程在等待,非公平鎖的不公平之處就在於此。回看下非公平鎖的加鎖流程,線程在進入同步隊列等待之前有兩次搶佔鎖的機會:
- 第一次是非重入式的獲取鎖,只有在當前鎖未被任何線程佔有(包括自身)時才能成功;
- 第二次是在進入同步隊列前,包含所有情況的獲取鎖的方式
只有這兩次獲取鎖都失敗後,線程纔會構造結點並加入同步隊列等待。而線程釋放鎖時是先釋放鎖(修改 state 值),然後才喚醒後繼結點的線程的。試想下這種情況,線程 A 已經釋放鎖,但還沒來得及喚醒後繼線程 C,而這時另一個線程 B 剛好嘗試獲取鎖,此時鎖恰好不被任何線程持有,它將成功獲取鎖而不用加入隊列等待。線程 C 此後才被喚醒嘗試獲取鎖,而此時鎖已經被線程B搶佔,故而其獲取失敗並繼續在隊列中等待。
如果以線程第一次嘗試獲取鎖到最後成功獲取鎖的次序來看,非公平鎖確實很不公平。因爲在隊列中等待很久的線程相比還未進入隊列等待的線程並沒有優先權,甚至競爭也處於劣勢:在隊列中的等待的線程要等待前驅結點線程的喚醒,在獲取鎖之前還要檢查自己的前驅結點是否爲頭結點。在鎖競爭激烈的情況下,在隊列中等待的線程可能遲遲競爭不到鎖。這也就非公平在高併發情況下會出現的飢餓問題。
爲什麼非公平鎖性能好
非公平鎖對鎖的競爭是搶佔式的(對於已經處於等待隊列中線程除外),線程在進入等待隊列之前可以進行兩次嘗試,這大大增加了獲取鎖的機會。這種好處體現在兩個方面:
- 線程不必加入等待隊列就可以獲得鎖,不僅免去了構造結點並加入隊列的繁瑣操作,同時也節省了線程阻塞喚醒的開銷,線程阻塞和喚醒涉及到線程上下文的切換和操作系統的系統調用,是非常耗時的。在高併發情況下,如果線程持有鎖的時間非常短,短到線程入隊阻塞的過程超過線程持有並釋放鎖的時間開銷,那麼這種搶佔式特性對併發性能的提升會更加明顯;
- 減少 CAS 競爭。如果線程必須要加入阻塞隊列才能獲取鎖,那入隊時 CAS 競爭將變得異常激烈,CAS 操作雖然不會導致失敗線程掛起,但不斷失敗自旋導致的對 CPU 的浪費也不能忽視。
AQS 在其他同步工具上的應用
在 ReentrantLock 的自定義同步器實現中,同步狀態表示鎖被一個線程重複獲取的次數。除了 ReentrantLock,AQS 也被大量應用在其他同步工具上。
ReentrantReadWriteLock
:ReentrantReadWriteLock
類使用 AQS 同步狀態中的低 16 位來保存寫鎖持有的次數,高16位用來保存讀鎖的持有次數。由於 WriteLock 也是一種獨佔鎖,因此其構建方式同 ReentrantLock。ReadLock 則通過使用 acquireShared 方法來支持同時允許多個讀線程。
AQS 同步狀態
Semaphore:Semaphore類(信號量)使用 AQS 同步狀態來保存信號量的當前計數。它裏面定義的 acquireShared 方法會減少計數,或當計數爲非正值時阻塞線程;tryRelease 方法會增加計數,在計數爲正值時還要解除線程的阻塞。
CountDownLatch:CountDownLatch 類使用 AQS 同步狀態來表示計數。當該計數爲 0 時,所有的 acquire 操作(對應到CountDownLatch
中就是 await 方法)才能通過。
FutureTask:FutureTask 類使用 AQS 同步狀態來表示某個異步計算任務的運行狀態(初始化、運行中、被取消和完成)。設置(FutureTask 的 set 方法)或取消(FutureTask 的 cancel 方法)一個 FutureTask 時會調用 AQS 的 release 操作,等待計算結果的線程的阻塞解除是通過 AQS 的 acquire 操作實現的。
SynchronousQueues:SynchronousQueues 類使用了內部的等待節點,這些節點可以用於協調生產者和消費者。同時,它使用 AQS 同步狀態來控制當某個消費者消費當前一項時,允許一個生產者繼續生產,反之亦然。