1、AQS簡介
AQS全名:AbstractQueuedSynchronizer,它就是Java的一個抽象類,它的出現是爲了解決多線程競爭共享資源而引發的安全問題,細緻點說AQS具備一套線程阻塞等待以及被喚醒時鎖分配的機制,這個機制AQS是用CLH隊列鎖實現的,即將暫時獲取不到鎖的線程加入到隊列中,隊列是雙向隊列。
常用的實現類是ReentrantLock和CountdownLatch。而且這兩個類都是通過內部類繼承AbstractQueuedSynchronizer,從而實現相應功能的。
先看下這張圖,對Java的重入鎖的代碼結構有個大概的瞭解。
內部抽象類Sync繼承AbstractQueuedSynchronizer,CountdownLatch一樣
我們一般使用AQS功能的簡單代碼實現:
public class Demo {
static Lock lock = new ReentrantLock();
public static void test() {
lock.lock();
try {
// TODO:
}
catch (InterruptedException ex) {
}
finally {
lock.unlock();
}
}
}
重入鎖通過加鎖lock 和 解鎖unlock操作進行多線程的同步控制操作。從上面代碼我們可以猜想到,在多線程競爭情況下,當線程加鎖操作獲取不到鎖,則線程要進入阻塞隊列;當鎖釋放後,隊列節點(線程)要能夠獲得鎖;那麼問題來了:
1、線程獲取到鎖具體是怎麼實現的?
2、線程獲取不到鎖具體是怎麼操作的?
3、鎖釋放後,隊列節點是怎麼觸發獲取鎖的?
這些細節問題,一定要看源碼才能得到答案。
2、源碼分析
2.1 線程阻塞
以公平鎖爲例:
java.util.concurrent.locks.ReentrantLock$FairSync
static final class FairSync extends Sync {
private static final long serialVersionUID = -3000897897090466540L;
final void lock() {
acquire(1); // 父類方法,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();
// 獲取鎖的狀態,其實就是計數器,0表示鎖沒有被任何線程獲得
int c = getState();
if (c == 0) {
// 這裏有三個方法:
// 1.hasQueuedPredecessors 有沒有前序結點,如果有肯定輪不到當前線程,
// 這個方法比較巧妙,需要先理解隊列設計思想才能看懂,後面有圖分析
// 2.compareAndSetState 設置鎖計數器=1,原子操作
// 3.setExclusiveOwnerThread 設置獨佔
// 三個操作都符合條件纔算當前線程獲得鎖
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
// 如果當前線程已經獲得鎖,那麼鎖計數器state加1
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
}
總結:AQS獲取鎖的機制就是維護一個int屬性state,
- state=0:表示鎖沒人在用
- state>0:表示有線程獲得鎖,state的值表示線程重入的次數,即多次獲得鎖。
java.util.concurrent.locks.AbstractQueuedSynchronizer
/**
* The synchronization state.
*/
private volatile int state;
public final void acquire(int arg) {
// 子類(公平/非公平)調用父類的這個方法
// 如果tryAcquire嘗試加鎖成功就沒有後面方法什麼事了
// 如果tryAcquire嘗試加鎖是失敗,則先addWaiter把線程加到隊列,然後再acquireQueued嘗試獲取鎖。。。
if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
// 如果線程被中斷過,再調用一次interrupt方法,清楚中斷狀態
selfInterrupt();
}
/**
* Creates and enqueues node for current thread and given mode.
*
* @param mode Node.EXCLUSIVE for exclusive, Node.SHARED for shared
* @return the new node
*/
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
Node pred = tail; // 找尾結點,可能是null,比如:第二個線程過來
if (pred != null) {
// 如果排隊的線程很多,給當前線程Node設置好前序結點
node.prev = pred;
// compareAndSetTail是將tail指向當前Node,這裏是原子操作
if (compareAndSetTail(pred, node)) {
pred.next = node; // 給原尾結點設置後序結點,也就是當前Node
return node; // 設置完就return
}
}
// 當pred先序結點爲空的時候,有可能需要初始化隊列
enq(node);
return node;
}
/**
* Inserts node into queue, initializing if necessary. See picture above.
* @param node the node to insert
* @return node's predecessor
*/
private Node enq(final Node node) {
for (;;) { // 死循環操作
Node t = tail;
// 可以看到這裏依然對tail做了if-else判斷,爲啥呢?
// 因爲多線程,這裏有可能tail就是非空,所以上面方法說有可能初始化
if (t == null) { // Must initialize
// 初始化分支
// compareAndSetHead是個原子操作,反正CAS開頭的都是原子操作
// 需要注意的是,這裏不是用的方法參數node,而是先創建了一個Node,並且head,tail都指向了這個空Node
if (compareAndSetHead(new Node()))
tail = head;
} else {
// 已經初始化分支
// 這裏把node設置成tail結點
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t; //返回原來的tail,初始化的時候並不關心返回值,只在xxx的時候關心
}
}
}
}
/**
* Acquires in exclusive uninterruptible mode for thread already in
* queue. Used by condition wait methods as well as acquire.
*
* @param node the node
* @param arg the acquire argument
* @return {@code true} if interrupted while waiting
*/
// 核心方法
final boolean acquireQueued(final Node node, int arg) {
// 到這裏,總結一下addWaiter方法
// addWaiter方法做的事情就是把當前線程封裝成一個node,加入到隊列中,並返回
boolean failed = true;
try {
boolean interrupted = false;
// 又一個死循環,直到return interrupted獲取鎖
for (;;) {
// 這個for循環的意思是:
// 1、如果當前線程確定還輪不到獲取鎖,則乖乖地進入隊列,並且當前線程中斷
// 2、如果當前線程可以獲取鎖,則死循環反覆去嘗試獲取鎖,當然,在公平鎖模式下一次就OK了
final Node p = node.predecessor(); // 取前序結點
// 如果前序節點是head節點,則嘗試獲取鎖,即第二次嘗試獲取鎖(第一次是tryAcquire)
if (p == head && tryAcquire(arg)) { // 第二次tryAcquire,也就是在入隊列前再嘗試一把,萬一鎖被釋放了呢!
// 如果獲取到鎖,把head指向當前node,把初始化創建的空節點GC
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted; // 這個interrupted表示的是當前線程有沒有被中斷過
}
// shouldParkAfterFailedAcquire返回true表示前序節點還在排隊,所以當前節點需要去park,進到parkAndCheckInterrupt方法
if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed) // 如果上面代碼沒有獲取鎖報錯,需要取消獲取鎖的動作
cancelAcquire(node); // 取消
}
}
// 獲取鎖失敗後應該排隊
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
// 走進這個方法說明前面沒有獲得鎖
// 先拿到前序節點的狀態
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
// 前序節點還在排隊呢,所以當前節點node只能掛起,安心地去排隊
// 這裏說下節點得SIGNAL狀態,它的意思是如果鎖被釋放,應該通知SIGNAL狀態的節點
/*
* This node has already set status asking a release
* to signal it, so it can safely park.
*/
return true;
// 下面的情況,node節點不需要去park,最終返回false使上層調用方法死循環直到獲取鎖
// 首先是ws>0,即waitStatus=cancelled=1(看內部類:Node)
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只能是0/-3,
/*
* 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.
*/
// 把前序節點的waitStatus設置爲-1:SIGNAL,因爲啥呢?
// 看上面if (ws == Node.SIGNAL)分支,只有前序節點waitStatus=-1,當前節點才能安心地去隊列等待,
// 否則當前node會一直自旋獲取鎖,這顯然是不合理的
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
/**
* Convenience method to park and then check if interrupted
* 調用底層線程掛起方法將線程掛起,並且返回掛起的狀態,也就是檢查一下
*
* @return {@code true} if interrupted
*/
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this); // 這裏直接調park方法是將線程掛起
return Thread.interrupted();
}
總結:阻塞等待
AQS使用FIFO雙向阻塞隊列來保存被阻塞的線程,實現機制是,AQS通過其內部類Node封裝線程,同時Node維護prev,next,waitStatus信息來實現雙向隊列;
針對節點的waitStatus屬性(等待狀態),要補充說明一下,它的取值有以下幾種:
- CANCELLED = 1; // 取消狀態,唯一個大於0的狀態,表示節點獲取鎖超時。例如:1,2,3三個節點按順序獲取鎖,結果1正在處理業務,2,3排隊等待,2先來的,等久了超時了,而3沒超時,等1釋放鎖以後,3雖然排在2後面,但是會把CANCELLED狀態的2節點刪掉,讓後面未取消的節點頂上來;
- SIGNAL = -1; // 等待觸發狀態,這個狀態的節點就是鎖釋放後需要被通知的節點;
- CONDITION = -2; // 等待條件狀態,還沒研究到,先按下不表;
- PROPAGATE = -3; // 狀態需要向後傳播
- 0;未賦值初始化爲0
下面畫圖來說明以下阻塞隊列的初始化和變化:
1、隊列初始化
此時會構造一個空節點放進隊列,鎖的head和tail都指向這個空節點,空節點的thread,prev,next都是null;鎖第一次被獲得就會構造帶有一個空節點的隊列,當然當前線程直接就獲得鎖了,而不會入到隊列。
2、如果有線程競爭,獲取不到鎖的線程就會被封裝成Node節點入到隊列中去,但不是替換空節點,而是跟在空節點的後面;
3、現在鎖釋放了,節點1獲得鎖了,看看隊列的變化,隊列會把head指向節點1,原來的空節點就等着被GC,節點1的thread,prev會被置空,next不變,因爲如果節點1後面還有節點2的話,next就指向節點2;
總之一句話,除了初始化存在空節點以外,隊列的head節點總是最後一個獲得鎖的節點;
2.2 線程喚醒
線程喚醒的前提當然是線程被掛起,線程掛起操作在上面源碼中有貼出來,線程掛起後,線程也就阻塞在這裏:
LockSupport.park(this); // 這裏直接調park方法是將線程掛起
上面acquireQueued方法是AQS的核心,其線程阻塞與獲取鎖都在這個裏面,核心思想是:
1、如果前序節點還在排隊(waitStatus=-1),後續節點直接掛起;
2、如果前序節點取消了(waitStatus=1),後續節點的邏輯中會把取消的前序節點刪除(遞歸刪除);
3、如果前序節點也是剛加進來的,節點狀態還沒定,也沒有獲得鎖,那麼當前線程要把前序節點的waitStatus設置爲-1;
——第3點要好好理解,換句話說,隊列裏除了最後一個節點,其他節點的狀態都是由其next節點來修改的。爲啥要這樣做呢?這是因爲掛起的線程要解除掛起狀態獲取鎖,這需要一個狀態,看下面代碼分析。
java.util.concurrent.locks.ReentrantLock$FairSync
public void unlock() {
sync.release(1);
}
java.util.concurrent.locks.AbstractQueuedSynchronizer
public final boolean release(int arg) {
// 首先是嘗試釋放鎖,有人問了,這釋放鎖還需要嘗試嗎?又不是獲取鎖,還可能獲取不到
// 那確實存在釋放不了的情況,什麼情況呢? 那就是重入次數大於1的情況,按照重入鎖的設計,重入幾次就需要釋放幾次
if (tryRelease(arg)) {
// 鎖釋放成功,要喚醒後序掛起線程
Node h = head; // 這裏拿到head節點
if (h != null && h.waitStatus != 0) // 判斷head節點狀態非0,即被修改過
// 如果h.waitStatus = 0,表示沒有後續節點,能理解不?看上面第三點
unparkSuccessor(h);
return true;
}
return false;
}
private void unparkSuccessor(Node node) {
// 注意參數是head節點,因爲喚醒後續節點總是從head往後找
/*
* 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)
// 還原head節點狀態爲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;
// 校驗狀態,因爲隊列裏面可能就沒有其他競爭線程,或者next節點取消了
if (s == null || s.waitStatus > 0) {
s = null;
// 咋整呢?從後往前遍歷,遍歷到最靠前的一個狀態正常的節點,這個節點就是要被喚醒的節點
// 這裏需要注意的是,這裏並沒有刪除取消的節點,因爲取消是在獲得鎖的邏輯裏面刪除的
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0) // 這裏就能理解爲啥都要給前序節點設置狀態等於-1了吧,爲啥不是-2,-3呢?因爲那倆有其他用處
s = t;
}
if (s != null)
LockSupport.unpark(s.thread); // 喚醒線程節點
}
java.util.concurrent.locks.ReentrantLock$Sync
// 這裏邏輯就很簡單了,判斷state的值即可
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;
}
最後再來個總結,要說AQS的原理,很多人會談到隊列,計數器,但是更爲底層的支撐,我認爲應該是CAS + LockSupport,如果你詳細看過源碼的話,會發現AQS到處都是CAS操作(CAS操作的本質就是樂觀+自旋),線程的掛起和喚醒則是通過LockSupport來處理。隊列只是其實現的一種方式,換句話說,即便用數組應該也能做的到。
AQS和Synchronized,要說這兩者的區別,有很多,我不想一一列舉了,意義不大,因爲隨着版本的演進,兩者有很多地方都在相互靠攏。
最後談一下LockSupport,其核心功能就park和unpark,掛起和解掛,我看有些文章說重入鎖的掛起不會有線程的用戶/內核態切換,這是錯的,不管是ReentrantLock還是Synchronized,只要發生鎖的競爭,最終都是會有線程的狀態切換的(自旋失敗)。
相對於wait/notify,LockSupport有很多優點,具體這裏不累贅了,只舉個例子吧:先做一次unpark,在做一次park,線程不會掛起;但是先做兩次unpark,再做兩次park,線程就會掛起。
比如說:你去澡堂洗澡,澡堂服務員要給你一個鑰匙牌子掛在手上,才能進澡堂子,但是AQS是一個特殊的澡堂,它只給一個線程服務,而且它只有一個牌子,可以理解爲:這個澡堂子只服務一個顧客,而且只有一個寄存櫃,也就是隻有一個牌子(鑰匙),那麼線程阻塞則好比你去洗澡,但是服務員沒有牌子給你,你只能等,等那個牌子被釋放,你可能問了,不是說只服務我一個顧客嗎? 是的,不錯,但是這個服務員缺心眼啊,它不支持重入啊,它並不會因爲你已經拿到唯一的牌子了,就讓你進去,所以這Java只能自己實現重入鎖了!
再看下上面兩個例子:
1、先做一次unpark,在做一次park
——unpark相當於洗好澡了,把牌子還回去,park相當於去洗澡,發現有牌子,直接拿着牌子去洗澡了,所以線程不會阻塞。
2、先做兩次unpark,再做兩次park
——做兩次unpark,因爲只有一個牌子,所以效果跟做一次unpark一樣,接着,第一次park可以拿到牌子,第二次park就拿不到牌子了,所以線程阻塞。這裏能看出來native代碼傻了吧!