【JavaSE】Lock鎖,獨佔鎖ReentrantLock的AQS原始碼,如何管理同步佇列。acquire方法和release方法
一、Lock鎖具體是如何實現的
由上一篇部落格解釋,實現Lock鎖的子類,實現了介面的所有方法。每個方法又都依賴Sync這個內部靜態類來實現的,所以主要看一下Sync這個內部靜態類。
abstract static class Sync extends AbstractQueuedSynchronizer
Sync繼承了AbstractQueuedSynchronizer這個抽象類,其實它是java語言中一個重要的佇列同步器,簡稱AQS。它是構建鎖或者其他同步元件的基礎框架(如ReentrantLock、ReentrantReadWriteLock、Semaphore等)。
下面先來看一下AQS中和Lock鎖有關的一些方法:
獨佔鎖(ReentrantLock)本篇講解獨佔鎖
void acquire(int arg) //獨佔式獲取同步狀態,如果獲取失敗則插入同步佇列進行等待。
void acquireInterruptibly(int arg) //與acquire方法相同,但在同步佇列中等待時可以響應中斷。
boolean tryAcquireNanos(int arg,long nanosTimeout) //在2的基礎上增加了超時等待功能,在超時時間內沒有獲 得同步狀態返回false
boolean tryAcquire(int arg) //獲取鎖成功返回true,否則返回false
boolean release(int arg) //釋放同步狀態,該方法會喚醒在同步佇列中的下一個節點。
共享鎖(ReentrantReadWriteLocK)
void acquireShared(int arg) //共享式獲取同步狀態,與獨佔鎖的區別在於同一時刻有多個執行緒獲取同步狀態。
void acquireSharedInterruptibly(int arg) //增加了響應中斷的功能
boolean tryAcquireSharedNanos(int arg,lone nanosTimeout) //在2的基礎上增加了超時等待功能
boolean releaseShared(int arg) //共享鎖釋放同步狀態。
上面程式碼的解析中提到了一個同步佇列,這個佇列就是來管理那些同時競爭一個鎖的時候,沒有競爭到鎖的執行緒,會進行排隊放在一個資料結構中進行管理。那麼這個資料結構是怎樣的形式存在的?
在AQS有一個靜態內部類Node,這是我們同步佇列的每個具體節點。在這個類中有如下屬性
volatile int waitStatus; // 節點狀態
volatile Node prev; // 當前節點的前驅節點
volatile Node next; // 當前節點的後繼節點
volatile Thread thread; // 當前節點所包裝的執行緒物件
Node nextWaiter; // 等待佇列中的下一個節點
可以初步推斷出這些沒有競爭到鎖的執行緒,會被封裝成一個節點,然後以雙向連結串列的形式管理起來。
節點狀態是由一個簡單的int型來儲存的,用來表示此執行緒目前所處的狀態:
int INITIAL = 0; // 初始狀態
int CANCELLED = 1; // 當前節點從同步佇列中取消
int SIGNAL = -1; // 後繼節點的執行緒處於等待狀態,如果當前節點釋放同步狀態會通知後繼節點,使得後繼 節點的執行緒繼續執行。
int CONDITION = -2; // 節點在等待佇列中,節點執行緒等待在Condition上,當其他執行緒對Condition呼叫了 signal()方法後,該節點將會從等待佇列中轉移到同步佇列中,加入到對同步狀態的獲取中。
int PROPAGATE = -3; // 表示下一次共享式同步狀態獲取將會無條件地被傳播下去。
在佇列中的管理,主要依靠這個int型的變數。
另外AQS中有兩個重要的成員變數:
private transient volatile Node head;
private transient volatile Node tail;
由此可知,整個雙向連結串列是由頭尾節點來管理的。這樣更加方便執行緒的入隊和出隊操作。
那麼,節點如何進行入隊和出隊操作?實際上這對應著鎖的獲取和釋放兩個操作:獲取鎖失敗進行入隊操作,獲取 鎖成功進行出隊操作。
二、獨佔鎖ReentrantLock
現在我們來看一下ReentrantLock中的一些加鎖和解鎖操作的原始碼是怎樣的。
1.獨佔鎖的獲取
呼叫lock()方法是獲取獨佔鎖,獲取失敗就將當前執行緒加入同步佇列,成功 則執行緒執行。來看ReentrantLock原始碼:
public void lock() {
sync.lock();
}
ReentrantLock的lock呼叫了sync的lock,我們看看sync是怎麼來的:
//構造方法
public ReentrantLock() {
sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
構造方法中由例項化了NonfairSync或FairSync的物件,它倆又繼承了靜態內部類Sync
static final class NonfairSync extends Sync
static final class FairSync extends Sync
Sync中的lock是一個抽象方法:
abstract void lock();
所以在ReentrantLock中的lock看似是sync呼叫的lock,實則是由ReentrantLock的有參和無參構造方法決定的,sync子類所實現的lock方法(這裡以NonfairSync為例子)
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
lock方法使用CAS來嘗試將同步狀態改為1,如果成功則將同步狀態持有執行緒置為當前執行緒。否則將呼叫AQS提供的 acquire()方法
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
記住這個方法,下面將這個方法中的每個函式都解釋過去。
①.tryAcquire(arg)方法
再次嘗試獲取鎖,如果獲取成功,這個if條件直接跳出,執行緒獲取到鎖,如果失敗再執行後面的條件。看一下tryAcquire(arg)方法:
protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}
這是超類AQS中的,我們要看子類NonfairSync中所覆寫的方法:
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
呼叫了父類Sync中的nonfairTryAcquire(acquires)方法:
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
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;
}
通過原始碼可以發現這個方法,沒有排隊操作,直接判斷此時鎖的狀態是否可以獲取,或者此執行緒是否已經持有鎖(可重入)。如果都沒有,才返回false。
②addWaiter(Node.EXCLUSIVE)
如果tryAcquire返回false下一個呼叫的方法就是這個方法。先看傳入的引數:
static final Node EXCLUSIVE = null;
預設值是null。看一下這個方法的原始碼:
private Node addWaiter(Node mode) {
// 將當前執行緒包裝稱為Node型別
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
// 上面一句翻譯:嘗試enq的快速路徑;失敗時備份到完整enq
Node pred = tail;
// 當前尾節點不為空
if (pred != null) {
// 將當前執行緒以尾插的方式插入同步佇列中
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
// 當前尾節點為空或CAS尾插失敗
enq(node);
return node;
}
分析上面的程式碼,首先清楚這個方法要幹嘛,剛才申請鎖失敗,所以這個方法要把當前執行緒放到同步佇列,等待獲取鎖。那麼剛進去就將當前執行緒封裝成一個節點,可以更好的管理。然後看看當前鎖佇列中的尾節點tail是否為null。
1.如果為空就直接呼叫enq方法。
2.如果不為空,就嘗試將當前執行緒的節點尾插到同步佇列中,如果插入成功說明排隊成功返回當前節點。否則插入失敗,呼叫enq方法
看一下enq方法的原始碼:
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) { // Must initialize
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
我們已經知道此雙向連結串列是帶頭尾節點,而頭尾節點初始化的時機就是當尾節點tail為null的時候。所以在enq中進行死迴圈尾插,如果尾節點為null,就先用compareAndSetHead(new Node())方法初始化一個,當初始化成功,尾節點也被頭節點賦值,這時尾節點不為null,下一次迴圈就可以用CAS操作嘗試將本執行緒的節點尾插到同步佇列。因為是死迴圈,這個方法一定會等到尾插成功,才會返回尾節點。
③.acquireQueued(final Node node, int arg)
如果尾插成功,最終addWaiter(Node mode)返回當前插入執行緒的那個節點,接下來就呼叫acquireQueued,從名字就可以看出,這個方法是尾插隊成功的執行緒節點們排隊獲取鎖的過程,原始碼如下:
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);
// 釋放前驅節點 方便GC回收
p.next = null; // help GC
failed = false;
return interrupted;
}
// 獲取同步狀態失敗,執行緒進入等待狀態等待獲取獨佔鎖
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
程式邏輯通過註釋已經標出,整體來看這是一個這又是一個自旋的過程(for ( ; ; )),程式碼首先獲取當前節點的先驅 節點,如果先驅節點是頭結點, 並且成功獲得同步狀態的時候(if (p == head && tryAcquire(arg))),當前節點所 指向的執行緒能夠獲取鎖。反之,獲取鎖失敗進入等待狀態。
那麼當獲取鎖失敗的時候會呼叫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.獨佔鎖的釋放
獨佔鎖的釋放呼叫unlock方法,而該方法實際呼叫了AQS的release方法。下面來看這兩個方法的原始碼:
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;
}
這段程式碼邏輯就比較容易理解了,如果同步狀態釋放成功(tryRelease返回true)則會執行if塊中的程式碼,tryRelease的原始碼還是看AQS子類Sync中的tryRelease
①.tryRelease方法
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;
}
由原始碼可知,判斷當前執行緒是否是持有鎖的執行緒,如果不是就丟擲異常。如果是就判斷c==0,也就是說getState必須返回1。如果是,就將當前鎖的持有者設定為null,然後返回true,否則就改變setState狀態,然後返回false。
如果返回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)
LockSupport.unpark(s.thread);
}
首先獲取頭節點的後繼節點,當後繼節點為null的時候會呼叫LookSupport.unpark()方法,該方 法會喚醒該節點的後繼節點所包裝的執行緒。因此,每一次鎖釋放後就會喚醒佇列中該節點的後繼節點所引用的線 程,從而進一步可以佐證獲得鎖的過程是一個FIFO(先進先出)的過程
三、獨佔鎖獲取和釋放的總結
- 執行緒獲取鎖失敗,執行緒被封裝成Node進行入隊操作,核心方法在於addWaiter()和enq(),同時enq()完成對同步隊 列的頭結點初始化工作以及CAS操作失敗的重試;
- 執行緒獲取鎖是一個自旋的過程,當且僅當 當前節點的前驅節點是頭結點並且成功獲得同步狀態時,節點出隊即 該節點引用的執行緒獲得鎖,否則,當不滿足條件時就會呼叫LookSupport.park()方法使得執行緒阻塞;
- 釋放鎖的時候會喚醒後繼節點;
- 總體來說:在獲取同步狀態時,AQS維護一個同步佇列,獲取同步狀態失敗的執行緒會加入到佇列中進行自旋;移除 佇列(或停止自旋)的條件是前驅節點是頭結點並且成功獲得了同步狀態。在釋放同步狀態時,同步器會呼叫 unparkSuccessor()方法喚醒後繼節點。
四、Lock獨佔鎖的特性
還有中斷鎖lockInterruptibly()和超時等待鎖.tryLock(timeout,TimeUnit),這兩個方法也都是依靠AQS的原始碼實現的,如果學習了上面的原始碼,接下來這兩個原始碼就不難理解了,有興趣的可以自行檢視一下。
相關文章
- 抽象佇列同步器(獨佔鎖)抽象佇列
- AQS原始碼深入分析之獨佔模式-ReentrantLock鎖特性詳解AQS原始碼模式ReentrantLock
- 逐行分析AQS原始碼(2)——獨佔鎖的釋放AQS原始碼
- Lock的獨佔鎖和共享鎖的比較分析
- 【JavaSE】Lock鎖和synchronized鎖的比較,lock鎖的特性,讀寫鎖的實現。Javasynchronized
- 深入學習Lock鎖(1)——佇列同步器佇列
- 結合ReentrantLock獲得鎖分析AQS,lock過程分析ReentrantLockAQS
- 深入理解Java併發框架AQS系列(三):獨佔鎖(Exclusive Lock)Java框架AQS
- 圖解AQS系列(上)--獨佔鎖圖解AQS
- java併發程式設計 | 鎖詳解:AQS,Lock,ReentrantLock,ReentrantReadWriteLockJava程式設計AQSReentrantLock
- Java併發:深入淺出AQS之獨佔鎖模式原始碼分析JavaAQS模式原始碼
- 從ReentrantLock加鎖解鎖角度分析AQSReentrantLockAQS
- MySQL的共享鎖和獨佔鎖MySql
- Lock介面、重入鎖ReentrantLock、讀寫鎖ReentrantReadWriteLockReentrantLock
- 原始碼級深挖AQS佇列同步器原始碼AQS佇列
- Lock鎖相關以及AQSAQS
- Java併發包原始碼學習系列:ReentrantLock可重入獨佔鎖詳解Java原始碼ReentrantLock
- ReentrantLock基於AQS的公平鎖和非公平鎖的實現區別ReentrantLockAQS
- ReentrantReadWriterLock原始碼(state設計、讀寫鎖、共享鎖、獨佔鎖及鎖降級)原始碼
- AQS學習(一)自旋鎖原理介紹(為什麼AQS底層使用自旋鎖佇列?)AQS佇列
- 【Java併發】【AQS鎖】鎖在原始碼中的應用JavaAQS原始碼
- ReentrantLock可重入鎖——原始碼詳解ReentrantLock原始碼
- ReentrantLock 公平鎖原始碼 第0篇ReentrantLock原始碼
- ReentrantLock 公平鎖原始碼 第1篇ReentrantLock原始碼
- ReentrantLock 公平鎖原始碼 第2篇ReentrantLock原始碼
- AQS佇列同步器AQS佇列
- 關於 ReentrantLock 中鎖 lock() 和解鎖 unlock() 的底層原理淺析ReentrantLock
- 【Java】【多執行緒】同步方法和同步程式碼塊、死鎖Java執行緒
- AQS 自定義同步鎖,挺難的!AQS
- 【JavaSE】淺談偏向鎖、輕量級鎖和重量級鎖,如何獲取鎖,如何撤銷鎖。Java
- AbstractQueuedSynchronizer 佇列同步器(AQS)佇列AQS
- Java 佇列同步器 AQSJava佇列AQS
- 深圳某小公司面試題:AQS是什麼?公平鎖和非公平鎖?ReentrantLock?面試題AQSReentrantLock
- 001@多用派發佇列,少用同步鎖佇列
- 死磕java concurrent包系列(二)基於ReentrantLock理解AQS同步佇列的細節和設計模式JavaReentrantLockAQS佇列設計模式
- AQS原始碼探究之競爭鎖資源AQS原始碼
- Java JUC 抽象同步佇列AQS解析Java抽象佇列AQS
- Go和C語言的32 位的無鎖、併發、通用佇列的原始碼GoC語言佇列原始碼