ReentrantLock源碼剖析
這裏又是看了忘忘了看系列之ReetrantLock,今天趁着有時間記錄下ReentrantLock源碼的學習過程。這篇博客主要記錄以下幾個方面內容。歡迎各位多提建議或者意見
1、ReetrantLock和Sync的繼承結構
2、ReetrantLock構造函數們及AQS的核心屬性
3、ReetrantLock鎖的使用示例
4、ReetrantLock的原理及核心方法和設計思想
1、ReetrantLock和Sync的繼承結構
上圖就是ReetrantLock和Sync的繼承結構及關係,ReetrantLock內部有一個成員變量Sync,Sync又繼承自AQS(AbstractQueuedSynchronizer)。ReetrantLock內部同時也有兩個內部類FairSync和NonfairSync(都是Sync的子類),兩個類內部只有兩個方法,都是重寫了父類的方法,分別是lock()和tryAcquire()。
2、ReetrantLock構造函數們及AQS的核心屬性
2.1 先介簡單的紹構造函數
// 常用構造函數,內部創建非公平鎖
public ReentrantLock() {
sync = new NonfairSync();
}
// 通過手工指定是公平鎖還是非公平鎖,true爲公平鎖,false爲非公平鎖
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
2.2 介紹ReetrantLock的屬性及相關屬
- ReentrantLock自身屬性
/** Synchronizer providing all implementation mechanics */
private final Sync sync;
-
AQS的核心屬性
在介紹AQS的屬性之前先介紹下AQS的結構和用處,AQS是許多同步器的核心,內部大量使用了CAS機制。AQS是實現大部分同步需求的基礎。
同步器的主要使用方式是繼承,子類通過繼承並實現他的抽象方法來管理同步狀態。ReentrantLock、ReentrantReadWriteLock和CountDownLatch等同步器組件的核心都是AQS。
AQS使用隊列來管理等待鎖的線程,內部類似一個FIFO隊列,每一個節點使用Node來表示。結構如下圖所示。
// AQS下屬性爲同步器的核心屬性
private transient volatile Node head;
private transient volatile Node tail;
private volatile int state;
// Node結構中的屬性,本次只分析和ReetrantLock相關的屬性
static final class Node {
// 排他鎖模式
static final Node EXCLUSIVE = null;
// 取消狀態
static final int CANCELLED = 1;
// 此狀態表示其還有後續節點
static final int SIGNAL = -1;
// 等待狀態,會被賦值爲上面幾種狀態之一
volatile int waitStatus;
// 前置節點
volatile Node prev;
// 後置節點
volatile Node next;
// 節點所依附的線程
volatile Thread thread;
}
3、ReetrantLock鎖的使用示例
下面我來演示一個我們常用的示例,隨後通過這個示例來展開分析核心原理。
public class Test{
public static void main(String [] args){
// 創建鎖
ReentrantLock myLock = new ReentrantLock();
// 在需要的地方加鎖
myLock.lock();
try {
// 在加鎖後做一些感興趣的事情
System.out.println("fuhang do something");
throw new RuntimeException("Oh,that's too bad !");
}catch (Exception e){
// do something
}finally {
// 最後一定要手動釋放鎖
lock.unlock();
}
}
}
上面就是ReetrantLock的一個簡單示例,這裏插播記錄一個ReetrantLock對比Synchronized關鍵字的優勢,比如A線程要獲取B鎖,獲取到B鎖後需要再獲取C鎖,獲取到C鎖後需要釋放B鎖這種場景下,使用ReentrantLock就很好控制,而Synchronized相對來說就不是這麼方便了。
4、ReetrantLock的原理及核心方法和設計思想
下面我們剖析使用示例中的方法一步一步來學習ReetrantLock的原理。
4.1 加鎖過程
① 先調用ReetrantLock.lock()方法,這個方法內部調用了屬性變量sync的lock方法
// ReetrantLock 內部調用了Sync的子類的lock方法,在這裏就是NonfairSync類的lock方法
public void lock() {
sync.lock();
}
②調用Sync類的子類NonfairSync的lock()方法。
下面這個方法第一個if條件嘗試獲取鎖,在這裏獲取鎖就是使用 CAS 機制將 AQS 類中的 state 屬性變量從 0 變爲 1 (ReentrantLock中的同步器(Sync)的state屬性爲0 時候表示無鎖,大於 0 時候表示加鎖狀態),如果修改成功則表示獲取到鎖,然後調用AbstractOwnableSynchronizer.setExclusiveOwnerThread(Thread owner)
方法設置獲取到鎖的線程。
如果獲取鎖失敗則進入else代碼,執行acquire(1)。
// NonfairSync.lock()方法
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
③ 調用 acquire(1)
方法,在這個方法裏面直接進入if判斷,在if語句裏面先執行tryAcquire(arg)
方法嘗試再次獲取鎖和處理重入鎖邏輯,若失敗則再執行addWaiter()
和acquireQueued(Node,int)
方法。若兩個條件都爲真的情況下執行自我中斷方法selfInterrupt()
中斷當前線程。
public final void acquire(int arg) {
if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
④ 調用NonfairSync.tryAcquire(int)
方法,此方法內部調用nonfairTryAcquire(int)
方法。
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
final boolean nonfairTryAcquire(int acquires) {
// 獲取當前線程引用
final Thread current = Thread.currentThread();
// 獲取鎖的狀態
int c = getState();
// 如果是 0 則說明是無鎖狀態,CAS修改鎖狀態嘗試獲取鎖,成功返回true
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}else if (current == getExclusiveOwnerThread()) {
// 這裏是處理重入鎖的邏輯,重入一次就將state+1,最後返回true
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
// 既不是重入鎖也不可獲取到鎖的情況下返回false
return false;
}
⑤ 調用addWaiter(Node)方法。方法的簡介爲將當前線程以給定模式加入隊列,此處的Node參數用來表示模式,Node.EXCLUSIVE代表互斥鎖,Node.SHARED代表共享鎖。
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
// 嘗試快速加入隊列,若失敗則調用enq方法入隊
Node pred = tail; // 獲取隊尾元素
if (pred != null) { // 如果隊尾不爲空(也就是說隊列存在)
node.prev = pred; // 設置節點的前置節點未隊尾節點
if (compareAndSetTail(pred, node)) { // CAS設置隊尾元素爲node,其他線程可能在做同樣操作
pred.next = node; // 將原隊尾元素的next指向新的隊尾元素node
return node;
}
}
// 如果上面尾部不存在(隊列沒初始化)
// 或者CAS設置尾部時候失敗(說明存在競爭,別的線程設置成功了)
// 則調用enq(Node)方法將節點加入隊列
enq(node);
return node;
}
⑥ enq(Node) 方法,此方法用來初始化隊列或者死循環到成功將節點加入隊列
private Node enq(final Node node) {
for (;;) {
// 獲取尾部節點
Node t = tail;
// 如果尾部節點爲空,則嘗試初始化隊列
if (t == null) { // Must initialize
if (compareAndSetHead(new Node()))
tail = head;
} else {
// 下面邏輯和addWaiter()中快速入隊邏輯相同
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
⑦ acquireQueued(Node,int)
方法,此方法在嘗試獲取鎖失敗後並且addWaiter()方法成功將包裝當前線程的Node節點加入隊列後調用。這個方法是每個沒拿到鎖並且被加入到等待隊列的線程最後進入的方法,在這個方法裏面使用死循環來響應喚醒信號或者中斷信號。
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);
// 前置節點的下一個節點引用設爲null,幫助垃圾回收器回收
p.next = null; // help GC
// 將失敗標誌置位false,表示獲取鎖成功
failed = false;
// 返回中斷標誌,交由後續處理
return interrupted;
}
// 獲取鎖失敗後執行兩個方法,這兩個方法在下面分析
if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
interrupted = true;// 此標誌返回給最上層做後續處理
}
} finally {
// 如果執行到此失敗標誌爲true,則要做此節點的後續處理工作
if (failed)
cancelAcquire(node);
}
}
// 此方法將node節點設置爲頭節點,並把node自身的前置節點引用和線程引用置空
private void setHead(Node node) {
head = node;
node.thread = null;
node.prev = null;
}
⑧ shouldParkAfterFailedAcquire()
方法本意是判斷是否在獲取鎖失敗後應該阻塞線程,參數爲線程對應節點及前置節點。什麼樣的方法可以安全的被阻塞呢?
當一個節點的waitStatus爲SIGNAL時候,就表明它自身還有後續節點,在釋放鎖的時候需要喚醒後續節點。那一個節點能安全被阻塞就需要前置節點自身知道它有後續節點。那麼此方法主要確認前置節點的waitStatus是否爲SIGNAL,若不是則將前置節點的waitStatus修改爲SIGNAL並等待下次進入方法繼續判斷。
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
// 獲取前置節點的等待狀態,默認情況下爲 0
int ws = pred.waitStatus;
// 如果前置節點狀態爲SIGNAL,那麼說明本節點可以安全阻塞
// 此狀態下前序節點釋放鎖時候會喚醒後續節點
if (ws == Node.SIGNAL)
return true;
if (ws > 0) { // 如果前序節點的狀態大於 0 , 那麼就是 cancelled 待取消狀態
// 那麼嘗試往前繼續找狀態<=0的前置節點
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
// 找到後將前置節點的下一個節點指向本節點
pred.next = node;
} else {
// 走到這裏說明前置節點正常,但是前置節點狀態不爲SIGNAL
// 那麼將前置節點的狀態使用CAS設置爲SIGNAL,讓前序節點釋放鎖時候知道
// 它後面還有在等待的節點需要通知
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
// 返回false 等待下一次進入此方法
return false;
}
⑨ parkAndCheckInterrupt()
方法主要是爲了阻塞線程和響應喚醒及中斷信息。其中有個點需要講解下:Thread.interrupted()
方法是個靜態方法。
Thread.interrupted()
內部調用了currentThread().isInterrupted(true)
方法,此方法判斷當前線程線程的中斷標誌是否爲true,若爲true,返回當前線程中斷標誌並將當前線程標誌設置爲false,若爲false,返回當前線程中斷標誌後不做任何操作。若用戶線程只調用一次thread.interrupt()
方法,那麼第一次執行Thread.interrupted()
方法時候返回true,隨後無論執行多少次Thread.interrupted()
方法都返回false。
由此可知方法parkAndCheckInterrupt()
可使在用戶只調用一次thread.interrupt()
方法的情況下,if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
中它自身只返回一次true。這樣就不會重複執行滿足if條件後的語句了。
private final boolean parkAndCheckInterrupt() {
// 阻塞線程,執行此方法後線程會阻塞在這裏
LockSupport.park(this);
// 當調用 LockSupport.unpark(Thread)或者thread.interrupt()方法時候
// 被阻塞的線程會從上面方法返回,因此可以繼續執行下面程序
return Thread.interrupted();
}
4.2 解鎖過程
① 調用ReetrantLock.unlock()
方法,這個方法內部調用了sync.release()
public void unlock() {
sync.release(1);
}
② 本例中sync.release()
方法實際是調用了AQS類中的release()
方法
public final boolean release(int arg) {
// 嘗試釋放鎖
if (tryRelease(arg)) {
Node h = head;
// 如果頭不爲空,則說明有等待隊列
// 若在第一個條件爲true的條件下,waitStatus不爲0說明還有後繼節點需要喚醒
if (h != null && h.waitStatus != 0)
// 喚醒頭結點的後繼節點
unparkSuccessor(h);
return true;
}
return false;
}
③ 本例中tryRelease()實際調用的Reentrant.Sync.tryRelease()方法,此方法主要將同步器的狀態進行變更
protected final boolean tryRelease(int releases) {
// 當前狀態減去釋放值的結果賦值給c
int c = getState() - releases;
// 如果當前線程不是獲取鎖的線程,則拋出非法監視器狀態異常
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
// 標記是否是無鎖狀態
boolean free = false;
// 若c==0 則表示當前處於無鎖狀態
if (c == 0) {
free = true;
// 設置獲得鎖的線程爲Null
setExclusiveOwnerThread(null);
}
// 設置鎖狀態值爲c
setState(c);
// 返回free,若爲true則證明完全釋放鎖,若有後繼節點,可以喚醒後繼節點了
// 若爲false,則可能是重入鎖的案例,鎖還不能被其後繼節點獲取
return free;
}
④ 調用AQS類下的unparkSuccessor()
方法,此方法主要是處理好傳入節點本身的狀態,然後喚醒其後繼節點。
private void unparkSuccessor(Node node) {
// 獲取傳入節點的waitStatus
int ws = node.waitStatus;
// 如果waitStatus<0 那麼使用CAS將node的狀態設置爲0
// 使用CAS是擔心此時有其他節點修改傳入節點的waitStatus
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
// 獲取node節點的下一個節點
Node s = node.next;
// 若下一個節點爲空或者下一個節點waitStatus>0 (Cancelled狀態)
if (s == null || s.waitStatus > 0) {
// 先將s置空(s!=null && waitStatus>0)
s = null;
// 從等待隊列尾部開始遍歷,找到一個可用的(waitStatus<=0)節點賦值給s
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
// 如果後繼節點不爲空,喚醒後繼節點的線程
if (s != null)
LockSupport.unpark(s.thread);
}
總結
以上就是我個人理解的ReentrantLock的原理和核心方法,其他方法在以後的學習中會繼續記錄。以後每次學習後通過寫成文字的方式再梳理一遍,希望能有所收穫。如果我有新的理解也會不斷迭代此文章。