淺析抽象隊列同步器(AQS)
目錄
除非我不想贏,否則沒人能讓我輸。
複習多線程併發包總結
什麼是AQS
AQS(AbstractQueuedSynchronizer)是一個抽象隊列同步器,通過維護一個共享資源狀態(volatile int state)和一個FIFO線程等待隊列(底層是雙向鏈表)來實現一個多線程訪問共享資源的同步框架。許多同步類的實現都依賴於AQS,例如常用的ReentrantLock,CountDownLatch,Semaphore。
關於ReentrantLock,CountDownLatch,Semaphore的用法可參考:
[常用的三種同步類] https://blog.csdn.net/qq_42107430/article/details/103854488
JDK1.8源碼:
/**
* The synchronization state.
*/
private volatile int state;
static final class Node {
static final Node SHARED = new Node();
static final Node EXCLUSIVE = null;
static final int CANCELLED = 1;
static final int SIGNAL = -1;
static final int CONDITION = -2;
static final int PROPAGATE = -3;
volatile int waitStatus;
volatile Node prev; //前驅節點
volatile Node next; //後繼節點
volatile Thread thread;//當前線程
Node nextWaiter; //存儲在condition隊列中的後繼節點
//是否爲共享鎖
final boolean isShared() {
return nextWaiter == SHARED;
}
final Node predecessor() throws NullPointerException {
Node p = prev;
if (p == null)
throw new NullPointerException();
else
return p;
}
Node() { // Used to establish initial head or SHARED marker
}
//將線程構造成一個Node,添加到等待隊列
Node(Thread thread, Node mode) { // Used by addWaiter
this.nextWaiter = mode;
this.thread = thread;
}
//這個方法會在Condition隊列使用,後續單獨寫一篇文章分析condition
Node(Thread thread, int waitStatus) { // Used by Condition
this.waitStatus = waitStatus;
this.thread = thread;
}
}
AQS的原理
AQS爲每個共享資源變量設置一個共享資源鎖,線程在需要訪問共享資源時首先需要去獲取共享資源縮。
如果獲取成功,便可以在當前線程中使用該共享資源,如果獲取不成功,則將該線程放入線程等待隊列,等待下一次資源調度。
state狀態
AQS維護了一個volatile int 類型的變量,用於表示當前的同步狀態。Volatile雖然不能保證操作的原子性,但是可以保證操作的可見性。
state的訪問方式有以下三種,均是原子操作
-
getState()
-
setState()
-
compareAndSetState()
/**
* Returns the current value of synchronization state.
* This operation has memory semantics of a {@code volatile} read.
* @return current state value
*/
protected final int getState() {
return state;
}
/**
* Sets the value of synchronization state.
* This operation has memory semantics of a {@code volatile} write.
* @param newState the new state value
*/
protected final void setState(int newState) {
state = newState;
}
/**
* Atomically sets synchronization state to the given updated
* value if the current state value equals the expected value.
* This operation has memory semantics of a {@code volatile} read
* and write.
*
* @param expect the expected value
* @param update the new value
* @return {@code true} if successful. False return indicates that the actual
* value was not equal to the expected value.
*/
protected final boolean compareAndSetState(int expect, int update) {
// See below for intrinsics setup to support this
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
AQS的共享資源狀態:獨佔式和共享式
AQS定義兩種資源共享方式:獨佔式(Exclusive)和共享式(Share)。
-
獨佔式:只有一個線程能執行,如ReentrantLock
-
共享式:共享,多個線程可同時執行,如Semaphore/CountDownLatch
AQS只是一個框架,定義了一個接口,具體的資源獲取、釋放都交由自定義同步器實現。
不同的自定義同步器爭用共享資源的方式也不同。自定義同步器在實現時只需要實現共享資源state的獲取與釋放方式即可,至於具體線程等待隊列的維護(如獲取資源失敗入隊/喚醒出隊等),AQS已經在頂層實現好了。自定義同步器實現時主要實現以下幾種方法:
-
isHeldExclusively():該線程是否正在獨佔資源。只有用到condition才需要去實現它。
-
tryAcquire(int):獨佔方式。嘗試獲取資源,成功則返回true,失敗則返回false。
-
tryRelease(int):獨佔方式。嘗試釋放資源,成功則返回true,失敗則返回false。
-
tryAcquireShared(int):共享方式。嘗試獲取資源。負數表示失敗;0表示成功,但沒有剩餘可用資源;正數表示成功,且有剩餘資源。
-
tryReleaseShared(int):共享方式。嘗試釋放資源,如果釋放後允許喚醒後續等待結點返回true,否則返回false。
以ReentrantLock爲例,state初始化爲0,表示未鎖定狀態。A線程lock()時,會調用tryAcquire()獨佔該鎖並將state+1。此後,其他線程再tryAcquire()時就會失敗,直到A線程unlock()到state=0(即釋放鎖)爲止,其它線程纔有機會獲取該鎖。當然,釋放鎖之前,A線程自己是可以重複獲取此鎖的(state會累加),這就是可重入的概念。但要注意,獲取多少次就要釋放多麼次,這樣才能保證state是能回到零態的。否則程序運行時會報錯。
再以CountDownLatch以例,任務分爲N個子線程去執行,state也初始化爲N(注意N要與線程個數一致)。這N個子線程是並行執行的,每個子線程執行完後countDown()一次,state會CAS減1。等到所有子線程都執行完後(即state=0),會unpark()主調用線程,然後主調用線程就會從await()函數返回,繼續執行後續動作。
一般來說,自定義同步器要麼是獨佔方法,要麼是共享方式,他們也只需實現tryAcquire-tryRelease、tryAcquireShared-tryReleaseShared中的一種即可。但AQS也支持自定義同步器同時實現獨佔和共享兩種方式,如ReentrantReadWriteLock。
添加鎖和釋放鎖
當出現鎖競爭以及釋放鎖的時候,AQS同步隊列中的節點會發生變化。
添加節點
添加節點時會涉及到兩個變化
-
新的線程封裝成Node節點追加到同步隊列中,設置prev節點以及修改當前節點的前置節點的next節點指向自己
-
通過CAS講tail重新指向新的尾部節點
移除節點
head節點表示獲取鎖成功的節點,當頭結點在釋放同步狀態時,會喚醒後繼節點,如果後繼節點獲得鎖成功,會把自己設置爲頭結點
這個過程也是涉及到兩個變化
-
修改head節點指向下一個獲得鎖的節點
-
新的獲得鎖的節點,將prev的指針指向null
瞭解了AQS是什麼,原理實現後,我們結合ReentrantLock來深入理解AQS是如何實現線程安全的。
什麼是ReentrantLock
Java中除了使用關鍵字synchronized外,還可以使用ReentrantLock實現獨佔鎖的功能。而且ReentrantLock相比synchronized而言功能更加豐富,使用起來更爲靈活,也更適合複雜的併發場景。
實現
ReentrantLock繼承了Lock接口並實現了在接口中定義的方法。是一個可重入的獨佔鎖。通過自定義抽象隊列同步器來實現。
Lock接口JDK源碼
void lock() // 如果鎖可用就獲得鎖,如果鎖不可用就阻塞直到鎖釋放
void lockInterruptibly() // 和 lock()方法相似, 但阻塞的線程可中斷,拋出 java.lang.InterruptedException異常
boolean tryLock() // 非阻塞獲取鎖;嘗試獲取鎖,如果成功返回true
boolean tryLock(long timeout, TimeUnit timeUnit) //帶有超時時間的獲取鎖方法
void unlock() // 釋放鎖
如何使用
public class ReentrantLockDemo {
private static int count=0;
static Lock lock=new ReentrantLock();
public static void inc(){
lock.lock();
try {
Thread.sleep(1);
count++;
} catch (InterruptedException e) {
e.printStackTrace();
}finally{
lock.unlock();
}
}
這段代碼主要做一件事,就是通過一個靜態的incr()
方法對共享變量count
做連續遞增,在沒有加同步鎖的情況下多線程訪問這個方法一定會存在線程安全問題。所以用到了ReentrantLock
來實現同步鎖,並且在finally語句塊中顯式釋放鎖。
底層實現
ReentrantLock.lock()
public void lock() {
sync.lock();
}
可以看到lock()方法底層調用的是sync的lock()方法。
sync是一個靜態內部類,通過繼承AQS並實現了共享資源state的獲取和釋放的方式。
Sync
abstract static class Sync extends AbstractQueuedSynchronizer {
private static final long serialVersionUID = -5179523762034025860L;
/*定義一個抽象方法,由具體的子類去實現*/
abstract void lock();
/**
* 實現非公平的tryAcquire獲取資源
*/
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();//獲取當前線程
int c = getState();//獲取同步狀態
if (c == 0) {//如果狀態爲0 CAS設置acquires
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);//重新設置state
return true;
}
return false;
}
/*
* 嘗試釋放資源
*/
protected final boolean tryRelease(int releases) {
//計算要更新的同步狀態
int c = getState() - releases;
//如果當前線程不是排他線程 報錯
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
//如果狀態爲0,設置獨佔排他線程爲null,返回true
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
//更新同步狀態
setState(c);
return free;
}
protected final boolean isHeldExclusively() {
// While we must in general read state before owner,
// we don't need to do so to check if current thread is owner
return getExclusiveOwnerThread() == Thread.currentThread();
}
/*獲取當前線程*/
final Thread getOwner() {
return getState() == 0 ? null : getExclusiveOwnerThread();
}
/*獲取當前state狀態*/
final int getHoldCount() {
return isHeldExclusively() ? getState() : 0;
}
/*判斷是否被鎖定*/
final boolean isLocked() {
return getState() != 0;
}
}
Sync又有兩個具體的實現,分別是NofairSync(非公平鎖),FairSync(公平鎖)。
-
公平鎖 表示所有線程嚴格按照FIFO來獲取鎖
-
非公平鎖 表示可以存在搶佔鎖的功能,也就是說不管當前隊列上是否存在其他線程等待,新線程都有機會搶佔鎖
NofairSync
/**
* Sync object for non-fair locks 非公平鎖
*/
static final class NonfairSync extends Sync {
private static final long serialVersionUID = 7316153563782823691L;
/**
* 執行鎖定
*/
final void lock() {
//首先通過CAS設置state,如果成功,設置當前線程爲排他線程(非公平的關鍵)
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else//如果失敗再去嘗試獲得鎖 關於acquire的具體講解在下面
acquire(1);
}
/*調用父類sync的非公平tryAcquire獲取資源*/
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
lock()方法簡單解釋一下
-
由於這裏是非公平鎖,所以調用lock方法時,先去通過cas去搶佔鎖
-
如果搶佔鎖成功,保存獲得鎖成功的當前線程
-
搶佔鎖失敗,調用acquire來走鎖競爭邏輯
compareAndSetState調用的是Unsafe類的compareAndSetState方法進行原子操作
return unsafe.compareAndSetState(this, stateOffset, expect, update);
UnsafeUnsafe類是在sun.misc包下,不屬於Java標準。但是很多Java的基礎類庫,包括一些被廣泛使用的高性能開發庫都是基於Unsafe類開發的,比如Netty、Hadoop、Kafka等;Unsafe可認爲是Java中留下的後門,提供了一些低層次操作,如直接內存訪問、線程調度等
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
這個是一個native方法, 第一個參數爲需要改變的對象,第二個爲偏移量(即之前求出來的headOffset的值),第三個參數爲期待的值,第四個爲更新後的值整個方法的作用是如果當前時刻的值等於預期值var4相等,則更新爲新的期望值 var5,如果更新成功,則返回true,否則返回false;
FairSync
/**
* Sync object for fair locks
*/
static final class FairSync extends Sync {
private static final long serialVersionUID = -3000897897090466540L;
//嘗試獲取鎖
final void lock() {
acquire(1);
}
/**
* 公平版本的tryAcquire
* Fair version of tryAcquire.
*/
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
}
acquire
acquire是AQS中的方法,如果CAS操作未能成功,說明state已經不爲0,此時繼續acquire(1)操作,這裏大家思考一下,acquire方法中的1的參數是用來做什麼呢?如果沒猜中,往前面回顧一下state這個概念
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
這個方法的主要邏輯是
-
通過tryAcquire嘗試獲取獨佔鎖,如果成功返回true,失敗返回false
-
如果tryAcquire失敗,則會通過addWaiter方法將當前線程封裝成Node添加到AQS隊列尾部
-
acquireQueued,將Node作爲參數,通過自旋去嘗試獲取鎖。
addWaiter
private Node addWaiter(Node mode) { //mode=Node.EXCLUSIVE
//將當前線程封裝成Node,並且mode爲獨佔鎖
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
// tail是AQS的中表示同步隊列隊尾的屬性,剛開始爲null,所以進行enq(node)方法
Node pred = tail;
if (pred != null) { //tail不爲空的情況,說明隊列中存在節點數據
node.prev = pred; //講當前線程的Node的prev節點指向tail
if (compareAndSetTail(pred, node)) {//通過cas講node添加到AQS隊列
pred.next = node;//cas成功,把舊的tail的next指針指向新的tail
return node;
}
}
enq(node); //tail=null,將node添加到同步隊列中
return node;
}
ReentrantLock.unlock()
public void unlock() {
sync.release(1);
}
release
1 釋放鎖 ;2 喚醒park的線程
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
這個動作可以認爲就是一個設置鎖狀態的操作,而且是將狀態減掉傳入的參數值(參數是1),如果結果狀態爲0,就將排它鎖的Owner設置爲null,以使得其它的線程有機會進行執行。在排它鎖中,加鎖的時候狀態會增加1(當然可以自己修改這個值),在解鎖的時候減掉1,同一個鎖,在可以重入後,可能會被疊加爲2、3、4這些值,只有unlock()的次數與lock()的次數對應纔會將Owner線程設置爲空,而且也只有這種情況下才會返回true。
protected final boolean tryRelease(int releases) {
int c = getState() - releases; // 這裏是將鎖的數量減1
if (Thread.currentThread() != getExclusiveOwnerThread())// 如果釋放的線程和獲取鎖的線程不是同一個,拋出非法監視器狀態異常
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
// 由於重入的關係,不是每次釋放鎖c都等於0,
// 直到最後一次釋放鎖時,纔會把當前線程釋放
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
unparkSuccessor
在方法unparkSuccessor(Node)中,就意味着真正要釋放鎖了,它傳入的是head節點(head節點是佔用鎖的節點),當前線程被釋放之後,需要喚醒下一個節點的線程
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
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) //然後從隊列尾部向前遍歷找到最前面的一個waitStatus小於0的節點, 至於爲什麼從尾部開始向前遍歷,因爲在doAcquireInterruptibly.cancelAcquire方法的處理過程中只設置了next的變化,沒有設置prev的變化,在最後有這樣一行代碼:node.next = node,如果這時執行了unparkSuccessor方法,並且向後遍歷的話,就成了死循環了,所以這時只有prev是穩定的
s = t;
}
//內部首先會發生的動作是獲取head節點的next節點,如果獲取到的節點不爲空,則直接通過:“LockSupport.unpark()”方法來釋放對應的被掛起的線程,這樣一來將會有一個節點喚醒後繼續進入循環進一步嘗試tryAcquire()方法來獲取鎖
if (s != null)
LockSupport.unpark(s.thread); //釋放許可
}