序言
AQS可以說是JAVA原始碼中必讀原始碼之一。同時它也是JAVA大廠面試的高頻知識點之一。認識並瞭解它,JAVA初中升高階工程師必備知識點之一。 AQS是AbstractQueuedSynchronizer的簡稱,它也是JUC包下眾多非原生鎖實現的核心。
一:AQS基礎概況
AQS是基於CLH佇列演算法改進實現的鎖機制。大體邏輯是AQS內部有一個鏈型佇列,佇列結點類是AQS的一個內部類Node,形成一個類似如下Sync Queue(記住這個名詞)
可以看出,一個Node除了前後結點的索引外,還維護了一個Thread物件,一個int的waitStatus。 Thread物件就是處於競爭佇列中的執行緒物件本身。 waitStatus表示當前競爭結點的狀態,這裡暫且忽略掉。
處於隊首的,即Head所指向的結點,即為獲取到鎖的結點。釋放鎖即為出隊,後續結點則成為隊首,即獲取到鎖
Tips:這裡幫大家理解一個事情,每一個ReentrantLock例項都有且只有一個AQS例項,一個AQS例項維護一個Sync Queue。所以說,當我們的業務程式碼中的多個執行緒對同一個ReentrantLock例項進行鎖競爭操作時,其實際就是對同一個Sync Queue的佇列進行入隊、出隊操作。
二:AQS呼叫入口----ReentrantLock
我們在用ReentrantLock時,程式碼通常如下:
ReentrantLock lock = new ReentrantLock();
Runnable runnable = new Runnable() {
public void run() {
lock.lock();
Sys.out(Thread.currentThread().name() + "搶到鎖");
lock.unlock();
}
};
Thread t1 = new Thread(runnable);
Thread t2 = new Thread(runnable);
t2.start();
t1.start();
複製程式碼
可見執行緒t1,t2競爭lock這個鎖。
lock.lock()
搶鎖
lock.unlock()
釋放鎖
來看入口函式lock
public void lock() {
sync.lock();
}
複製程式碼
sync
是ReentrantLock
內的一個繼承了AQS
的抽象類
abstract static class Sync extends AbstractQueuedSynchronizer {
}
複製程式碼
抽象類的具體實現是另兩個內部類NonFairSync
FairSync
,分別代表非公平鎖、公平鎖。
我們系統來看下繼承圖
ReentrantLock中的sync例項,預設是在建構函式中初始化的,
public ReentrantLock() {
sync = new NonfairSync();
}
複製程式碼
那我們就看預設的NonFairSync
的實現邏輯。
NonFairSync
當我們呼叫ReentrantLock.lock()
時,直接呼叫到的是NonFairSync.lock()
/**
* Performs lock. Try immediate barge, backing up to normal
* acquire on failure.
*/
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
複製程式碼
compareAndSetState(0, 1)
通過CAS(CAS是啥,這裡不講,參考樂觀鎖。具體Google吧)方式,設定AQS的int state
欄位為1。AQS內部就是通過這個state
是否為0來判斷當前鎖是否已經被執行緒獲取到。
返回true
,則說明獲取鎖成功,設定當前鎖的獨佔執行緒setExclusiveOwnerThreaThread.currentThread());
否則,acquire(1)
嘗試獲d(取鎖。這個方法會自旋、阻塞,一直到獲取鎖成功為止。這裡,傳進去的引數1,就參考樂觀鎖的版本欄位,同時,這裡,它還記錄了可重入鎖重複獲取到鎖的次數。只有釋放同樣次數才能最終釋放鎖。
具體看AQS的acquire()
的邏輯
AbstractQueuedSynchronizer.acquire()
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
複製程式碼
要注意,這個方法是忽略執行緒中斷的。
先看tryAcquire(arg)
,這個方法是AQS留給子實現類的口子,具體實現看NonFairSync
,它的實現裡直接呼叫了Sync.nonfairTryAcquire()
,
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;
}
複製程式碼
可以看到方法內,先是嘗試獲取state = 0
時的初始鎖,如果失敗,判斷當前鎖是否是被當前執行緒獲取,是的話,將acquire
時傳入的引數累加到state
欄位上。在這裡,這個欄位就是用來記錄重複獲取鎖的次數。
獲取失敗則返回false
回到acquire
在獲取失敗,返回false
後,才會繼續呼叫 &&
右邊的acquireQueued(addWaiter(Node.EXCLUSIVE), arg)
方法。
先看addWaiter
private Node addWaiter(Node mode) {
1: Node node = new Node(Thread.currentThread(), mode);
2: // Try the fast path of enq; backup to full enq on failure
3: Node pred = tail;
4: if (pred != null) {
5: node.prev = pred;
6: if (compareAndSetTail(pred, node)) {
7: pred.next = node;
8: return node;
9: }
10: }
11: enq(node);
12: return node;
13: }
複製程式碼
利用傳進來的Node.EXCLUSIVE
表示的排斥鎖引數以及當前執行緒例項初始化新Node
,第4-10
行程式碼是在Sync Queue
佇列內有競爭執行緒時進入。為空時會走到enq(node)
,這裡是在競爭為空時將競爭執行緒入隊的操作。
然後返回當前競爭的執行緒node
此時Sync Queue
如圖
返回acquire
接著看
addWaiter
返回的node
直接作為引數給了acquireQueued
,這個方法就是主要的node
競爭鎖方法。
final boolean acquireQueued(final Node node, int arg) {
// 獲取鎖成功失敗標記
boolean failed = true;
try {
// 當前競爭執行緒的中斷標記
boolean interrupted = false;
// 自旋競爭鎖,競爭不到鎖的話,執行緒又沒有中斷
// 則一直在這兒迴圈
for (;;) {
// 獲取當前執行緒的前驅結點
final Node p = node.predecessor();
// 如果前驅結點是頭結點,則嘗試去tryAcquire
// 這個邏輯我們之前看過,當前執行緒未獲取到鎖的情況下
// 在AQS的state欄位不為0時,則返回false
if (p == head && tryAcquire(arg)) {
// 進入到這裡,說明要不就是當前執行緒在重複獲取
// 要不就是前邊的結點釋放鎖,state 歸0,這裡獲取到
setHead(node);
p.next = null; // help GC
// 標識競爭鎖成功
failed = false;
// 這個方法不響應執行緒中斷,但是會返回執行緒在競爭鎖過程中
// 中斷標記返回
return interrupted;
}
// 若獲取鎖失敗,則到這裡。這裡的邏輯主要在If判斷
// 的兩個方法中,用來將當前執行緒掛起的,具體邏輯
// 看下面
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
複製程式碼
park就是停下的意思。所以這個方法從名字上也比較好理解,就是
掛起執行緒並且檢查執行緒的中斷狀態。這裡要注意,LockSupport.part(this)
方法是會線上程中斷時自動喚醒的
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
複製程式碼
這個方法傳入了當前競爭結點及其前驅結點
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
// 前驅結點的等待狀態。這裡只需要記住,我們這裡考慮的是
// 非共享鎖、非公平鎖的AQS。所以,只需要確保當前競爭
// 結點的前驅結點狀態為SIGNAL就好。剩下的狀態,
// 與我們此時研究的情況而言沒有用
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.
*/
// 如果前驅結點的status為0,則將其改為SIGNAL
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
複製程式碼
從shouldParkAfterFailedAcquire
可以看出來,在確保前驅結點status為SIGNAL
時,就可以放心的去unsafe.park()
了。之所以要為SIGNAL
,是因為這個狀態含義為:當前結點OVER時要喚醒後繼結點。
所以不難推出,我們的結點現在就park
在那了。等他前驅結點釋放鎖,或者自己interrupt來喚醒,但因為這個方法是無視中斷的,所以即使interrupt了,只是設定了一個標記位,但仍然在迴圈中。
這裡假設前驅結點獲取鎖後釋放,則當前結點在parkAndCheckInterrupt()
方法中被喚醒,而後再次迴圈for(;;)
,這次會在第一個if
中就進入,當前結點獲取到鎖,然後重置Head
指向的結點等,返回當前執行緒的中斷標記。
返回acquire
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
複製程式碼
if
中的selfInterrupt()
方法只是去重新設定當前執行緒的中斷標記位。這是因為獲取執行緒中斷狀態的方法,在返回狀態欄位的同時,也會重置欄位,所以需要標記後重新設定相應的值。
下面我們看下AQS釋放鎖的介面方法
ReentrantLock.unlock
public void unlock() {
sync.release(1);
}
複製程式碼
追進去
ReentrantLock.release
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
是在NonFairLock
中的實現的,如果是釋放成功,則在Head
存在並且狀態不為0(其實可以理解為值為SIGNAL
時)去喚醒Head
的後繼結點。
NonFailLock.tryRelease
下面看下NonFairLock
的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;
}
複製程式碼
可以看出,這個tryRelease
其實就是去判斷下是不是當前執行緒擁有鎖,是的話,判斷下當前的釋放鎖是否完全釋放,因為鎖可以重複獲取,完全釋放的話,就設定state
為0,代表AQS的鎖已經被釋放了。