什麼是AQS?AQS有什麼用呢?
本篇文章主要就是解決這兩個問題,並且附上源碼解析。
AQS 的全稱是 AbstactQueuedSynchronizer 即抽象隊列同步器。
可能大部分使用Java語言的同學都知道它,因爲他是面試的高頻問題之一,面試Android也會問這樣的問題,我自己就被問了好幾次。
java併發包下很多API都是基於AQS來實現的加鎖和釋放鎖等功能的,AQS是java併發包的基礎類。比如:ReetrantLock ,ReentrantReadWriteLock 都是基於AQS來實現的。
ReentrantLock 實現加鎖和鎖釋放就是通過AQS來實現的。
先看一段代碼:
private void doTask1(){
try {
reentrantLock.lock();
Log.e("aqs", "doTask1 獲得鎖");
Thread.sleep(3 * 1000);
// doTask2();
}catch (Exception e){
e.printStackTrace();
}finally {
reentrantLock.unlock();
Log.e("aqs", "doTask1 釋放鎖");
}
}
如上,如果一個線程調用 lock 會發生什麼呢?
其實這個不難,不要一涉及到AQS就覺得很難。AQS 中維護了一個很重要的變量 state, 它是int型的,表示加鎖的狀態,初始狀態值爲0;另外 AQS 還維護了一個很重要的變量exclusiveOwnerThread,它表示的是獲得鎖的線程,也叫獨佔線程。AQS中還有一個用來存儲獲取鎖失敗線程的隊列,以及head 和 tail 結點,包含 如下圖所示
這時,線程1 跑過來調用ReentrantLock的lock()方法嘗試進行加鎖,這個加鎖的過程,直接就是用CAS操作將state值從0變爲1。如果對CAS操作不理解的話,可以看看我之前的文章:我對CAS的理解和用法
如果這時候沒有其他的線程操作,那麼CAS操作肯定是成功的,然後設置 exclusiveOwnerThread 爲 當前線程。lock的代碼如下(這裏是以默認的非公平鎖爲例):
final void lock() {
//通過CAS操作 ,如果當前的state 等於0 那麼 cas 就會操作成功,返回true,表示當前線程成功獲取鎖
if (compareAndSetState(0, 1))
//設置exclusiveOwnerThread 爲當前線程
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
我們從ReentrantLock的名字就可以知道它是可以重入的,那麼它的重入是怎麼實現的呢?對的,就是跟 state 和 exclusiveOwnerThread 有關,具體是怎麼樣的呢?看下面的例子
private void doTask1(){
try {
reentrantLock.lock();
Thread.sleep(3 * 1000);
doTask2();
}catch (Exception e){
e.printStackTrace();
}finally {
reentrantLock.unlock();
}
}
private void doTask2(){
try {
reentrantLock.lock();
Thread.sleep(10 * 1000);
}catch (Exception e){
e.printStackTrace();
}finally {
reentrantLock.unlock();
}
}
線程 先執行 doTask1,然後doTask1中執行doTask2,由於是同一個線程和同一把鎖,所以就可以重入了。
具體流程就是:線程執行到doTask2的時候, 執行 lock 發現 state 已經不是0而是1了,然後檢查 當前線程是不是和獲取鎖的線程是同一個,結果發現是同一個,所以 state+1 = 2,這就是可重入的核心原理。源碼如下,在 ReentranLock 中,具體的調用關係 我就不列出來了。
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
//如果 state = 0,那麼 通過cas來操作獲取鎖,跟之前的流程一樣,這裏爲什麼還要執行同樣的操作呢?因爲可能執行到這裏的時候,上一個線程剛好執行完,state-- 等於0
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
//這裏就是可重入的邏輯呢,
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
//小於0表示可重入的次數大於int型最大值,產生溢出了。
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
那麼 此時,如果線程2 來調用 reentrntlock.lock()方法來獲取鎖會是什麼樣子的呢?
線程2跑過來一下看到,發現state的值不是0啊?所以CAS操作將state從0變爲1的過程會失敗,因爲state的值當前爲1,說明已經有人加鎖了!
接着線程2會看一下,是不是自己之前加的鎖啊?當然不是了,exclusiveOwnerThread這個變量明確記錄了是線程1佔用了這個鎖,所以線程2此時就是加鎖失敗。
加鎖失敗後是怎麼操作的呢? 加鎖失敗後 ,此時就要將自己放入隊列中來等待,等待線程1釋放鎖之後,自己就可以重新嘗試加鎖了。
具體的代碼如下:
public final void acquire(int arg) {
// tryAcquire 就是調用上面nonfairTryAcquire,由於是線程1沒有釋放,所以線程2 調用tryAcquire返回false, 接着調用 acquireQueued方法
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
acquireQueued 中先調用 addWaiter ,addWaiter 代碼如下:
private Node addWaiter(Node mode) {
Node node = new Node(mode);
for (;;) {
Node oldTail = tail;
//表示等待隊列裏有其他的線程在等待了,然後就是設置node爲尾結點
if (oldTail != null) {
//當前的node的PREV 指向 尾結點oldTail
U.putObject(node, Node.PREV, oldTail);
//把尾結點設置當前的node結點
if (compareAndSetTail(oldTail, node)) {
//之前的尾結點的next指向node
oldTail.next = node;
return node;
}
} else {//如果之前的等待隊列沒有等待的線程,那麼new一個node,讓head和tail指向這個new出來的結點
initializeSyncQueue();
}
}
}
上面的把node結點設置爲尾結點的操作不知道大家看明白沒?我畫個圖來說明下:
U.putObject(node, Node.PREV, oldTail);
compareAndSetTail(oldTail, node) 的操作如下,也是通過cas完成的,
oldTail.next = node; 就很簡單了,如下:
通過上面的3部操作就可以把 獲取鎖失敗的線程放到等待隊列的尾部。
接着看看 acquireQueued的源碼,如下:
final boolean acquireQueued(final Node node, int arg) {
try {
boolean interrupted = false;
for (;;) {
//判斷之前的結點是不是頭結點 head,如果是頭結點就嘗試去獲取鎖,
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
//獲取鎖成功的話,就把當前線程設置爲head
setHead(node);
//斷開之前頭結點
p.next = null; // help GC
return interrupted;
}
//如果之前的不是頭結點,那麼就要等待了,等候之前的線程釋放鎖後,調用 LockSupport來喚醒,
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} catch (Throwable t) {
cancelAcquire(node);
throw t;
}
}
接下來先看 shouldParkAfterFailedAcquire,如下:
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
// 注意Node的waitStatus字段我們在上面創建Node的時候並沒有指定 ,默認值是0
// waitStatus 的4種狀態
//static final int CANCELLED = 1;
//static final int SIGNAL = -1; //等待被喚醒
//static final int CONDITION = -2; //條件鎖使用
//static final int PROPAGATE = -3; //共享鎖時使用
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
return true;
// 如果 ws > 0,則表示是取消狀態,然後通過while循環 把所有是取消狀態的線程從等待隊列中刪除
if (ws > 0) {
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {//如果不是取消狀態,則通過cas操作將該線程的waitStatus設置爲等待喚醒狀態
pred.compareAndSetWaitStatus(ws, Node.SIGNAL);
}
return false;
}
上面的shouldParkAfterFailedAcquire方法只是將waitStatus設置爲SIGNAL,但是並沒有阻塞操作,真正的阻塞操作在下面的方法 parkAndCheckInterrupt,如下:
private final boolean parkAndCheckInterrupt() {
//阻塞當前線程,底層實現是unsafe
LockSupport.park(this);
//返回當前線程是否被中斷
return Thread.interrupted();
}
這裏對lock方法作以下總結:
- 當線程1調用lock方法時,首先看 AQS 的 state 是否爲0,如果是0的話,通過cas操作將state置爲1,並且設置獨佔線程爲當前線程
- 如果這時候線程1 要調用另外一個lock方法,就像我上面的例子那樣,那麼線程1會發現 state = 1,它再去看獨佔線程是不是就是自己,如果是的話 state + 1 ,獲取鎖成功。
- 如果線程1 執行的方法還沒有完成即鎖還沒有釋放,此時線程2調用lock方法,由於線程1沒有釋放鎖,那麼state不會等於0,且獨佔線程是線程1而不是自己(線程2),所以AQS會把線程2放到等待隊列的尾部,如果線程2的前置結點是頭結點head,那麼線程2會通過死循環一直去獲取鎖,如果不是頭結點那麼就會阻塞線程2,等待線程1釋放鎖且喚醒它。
這裏可能有點饒,看下我畫的圖。
線程鎖釋放是怎麼樣的呢?
我們知道是調用 unlock來實現的,具體是什麼樣的呢?其實很簡單 就是將 state-- 直到state = 0,然後通過 LockSupport.unpark()來喚醒等待隊列中的下一個結點。具體的看源碼:
public void unlock() {
sync.release(1);
}
這個沒啥子好說的,接着調用AQS的 release方法,如下:
public final boolean release(int arg) {
//如果釋放當前線程成功的話,那麼就去喚醒等待隊列的頭結點
if (tryRelease(arg)) {
Node h = head;
//頭結點不爲空且waitStatus不等於0
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
來看看 tryRelease 方法,實在Reentrantlock 中實現的,如下:
protected final boolean tryRelease(int releases) {
//state - 1
int c = getState() - releases;
//如果當前線程不是之前設置的獨佔線程則拋出鎖狀態異常
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {//如果c == 0表示當前線程已經釋放鎖了,然後設置獨佔線程爲null,如果不等於0說明當前線程執行了可重入操作,等可重入的方法執行完 調用 unlock方法,會執行本方法 state會等於0
free = true;
setExclusiveOwnerThread(null);
}
//state 值置爲 0
setState(c);
return free;
}
接下來看看 unparkSuccessor ,如下:
private void unparkSuccessor(Node node) {
//node 表示是頭結點,如果頭結點的 waitStatue < 0,則置爲0,
int ws = node.waitStatus;
if (ws < 0)
node.compareAndSetWaitStatus(ws, 0);
//s 表示的是頭結點的下一個結點。爲什麼是喚醒下一個結點而不是頭結點呢?
//因爲我們上面調用addWaiter方法的時候,如果等待隊列裏面沒有等待線程,那麼直接
//new 一個Node 然後 head 和 tail 都指向這個 node,換句話說這個頭結點只是用來佔位的,所以要從頭結點的下一個結點開始喚醒
Node s = node.next;
//waitStatus 大於0 表示該線程已經取消了,
if (s == null || s.waitStatus > 0) {
s = null;
//從隊列的尾部開始遍歷,找到一個waitStatue 小於等於0的線程來喚醒
for (Node p = tail; p != node && p != null; p = p.prev)
if (p.waitStatus <= 0)
s = p;
}
if (s != null)//喚醒線程
LockSupport.unpark(s.thread);
}
結語:AQS 結合 ReentrantLock的加鎖和解鎖已經介紹完了,有問題可以一起交流交流啊!