mysql面試指南-mysql版本類問題
AQS
<前一整子做了一個AQS的技術分享,特將內容整理記錄如下,_>
什麼是AQS?
AQS
的全稱是 AbstractQueuedSynchronizer
,即抽象佇列同步器
。是Java併發工具的基礎,採用樂觀鎖,通過CAS與自旋輕量級的獲取鎖。維護了一個volatile int state(代表共享資源)和一個FIFO執行緒等待佇列(多執行緒爭用資源被阻塞時會進入此佇列)。很多JUC包,比如ReentrantLock、Semaphore、CountDownLatch等併發類均是繼承AQS,通過AQS的模板方法,來實現的。
原理
AQS的組成結構
AQS = 同步狀態(volatile int state)
同步佇列(即等待佇列,FIFO的CLH佇列)
+ 條件佇列(ConditionObject)
- state:代表共享資源。
volatile
保證併發讀,CAS
保證併發寫 - 同步佇列(即等待佇列,CLH佇列):是CLH變體的虛擬雙向佇列(先進先出FIFO)來等待獲取共享資源。當前執行緒可以通過signal和signalAll將條件佇列中的節點轉移到同步佇列中
- 條件佇列(ConditionObject):當前執行緒存在於同步佇列的頭節點,可以通過await從同步佇列轉移到條件佇列中
同步狀態
在AQS中維護了一個同步狀態變數state,getState函式獲取同步狀態,setState、compareAndSetState函式修改同步狀態,對於AQS來說,執行緒同步的關鍵是對state的操作,可以說獲取、釋放資源是否成功都是由state決定的,比如state>0代表可獲取資源,否則無法獲取,所以state的具體語義由實現者去定義,現有的ReentrantLock、ReentrantReadWriteLock、Semaphore、CountDownLatch定義的state語義都不一樣。
- ReentrantLock的state用來表示是否有鎖資源
- ReentrantReadWriteLock的state高16位代表讀鎖狀態,低16位代表寫鎖狀態
- Semaphore的state用來表示可用訊號的個數
- CountDownLatch的state用來表示計數器的值
CLH佇列
CLH是AQS內部維護的FIFO(先進先出)雙端雙向佇列(方便尾部節點插入),基於連結串列資料結構,當一個執行緒競爭資源失敗,就會將等待資源的執行緒封裝成一個Node節點,通過CAS原子操作插入佇列尾部,最終不同的Node節點連線組成了一個CLH佇列,所以說AQS通過CLH佇列管理競爭資源的執行緒,CLH佇列具有如下幾個優點:
- 先進先出保證了公平性
- 非阻塞的佇列,通過自旋鎖和CAS保證節點插入和移除的原子性,實現無鎖快速插入
- 採用了自旋鎖思想,所以CLH也是一種基於連結串列的可擴充套件、高效能、公平的自旋鎖
Node內部類
Node
是AQS
的內部類,每個等待資源的執行緒都會封裝成Node
節點組成CLH
佇列、等待佇列,所以說Node
是非常重要的部分,理解它是理解AQS
的第一步。
waitStatus
nextWaiter
Node
在CLH
佇列時,nextWaiter
表示共享式或獨佔式標記 SHARED/EXCLUSIVENode
在條件佇列時,nextWaiter
表示下個Node
節點指標
條件佇列
Object的wait、notify函式是配合Synchronized鎖實現執行緒間同步協作的功能,A Q S的ConditionObject條件變數也提供這樣的功能,通過ConditionObject的await和signal兩類函式完成。
ConditionObject內部維護著一個單向條件佇列,不同於CLH佇列,條件佇列只入隊執行await的執行緒節點,並且加入條件佇列的節點,不能在CLH佇列, 條件隊列出隊的節點,會入隊到CLH佇列。
當某個執行緒執行了ConditionObject的await函式,阻塞當前執行緒,執行緒會被封裝成Node節點新增到條件佇列的末端,其他執行緒執行ConditionObject的signal函式,會將條件佇列頭部執行緒節點轉移到C H L佇列參與競爭資源,具體流程如下圖:
流程說明
執行緒獲取資源失敗,封裝成Node
節點從CLH
佇列尾部入隊並阻塞執行緒,某執行緒釋放資源時會把CLH
佇列首部Node
節點關聯的執行緒喚醒(此處的首部是指第二個節點,後面會細說),再次獲取資源。
入隊
獲取資源失敗的執行緒需要封裝成Node
節點,接著尾部入隊,在AQS
中提供addWaiter
函式完成Node
節點的建立與入隊。
/**
* @description: Node節點入隊-CLH佇列
* @param mode 標記Node.EXCLUSIVE獨佔式 or Node.SHARED共享式
*/
private Node addWaiter(Node mode) {
// 根據當前執行緒建立節點,等待狀態為0
Node node = new Node(Thread.currentThread(), mode);
// 獲取尾節點
Node pred = tail;
if (pred != null) {
// 如果尾節點不等於null,把當前節點的前驅節點指向尾節點
node.prev = pred;
// 通過CAS把尾節點指向當前節點
if (compareAndSetTail(pred, node)) {
// 之前尾節點的下個節點指向當前節點
pred.next = node;
return node;
}
}
// 如果新增失敗或佇列不存在,執行end函式
enq(node);
return node;
}
新增節點的時候,如果從CLH
佇列已經存在,通過CAS
快速將當前節點新增到佇列尾部,如果新增失敗或佇列不存在,則指向enq
函式自旋入隊。
/**
* @description: 自旋cas入隊
* @param node 節點
*/
private Node enq(final Node node) {
for (;;) { //迴圈
//獲取尾節點
Node t = tail;
if (t == null) {
//如果尾節點為空,建立哨兵節點,通過cas把頭節點指向哨兵節點
if (compareAndSetHead(new Node()))
//cas成功,尾節點指向哨兵節點
tail = head;
} else {
//當前節點的前驅節點設指向之前尾節點
node.prev = t;
//cas設定把尾節點指向當前節點
if (compareAndSetTail(t, node)) {
//cas成功,之前尾節點的下個節點指向當前節點
t.next = node;
return t;
}
}
}
}
通過自旋CAS
嘗試往佇列尾部插入節點,直到成功,自旋過程如果發現CLH
佇列不存在時會初始化CLH
佇列,入隊過程流程如下圖:
第一次迴圈
- 剛開始C L H佇列不存在,head與tail都指向null
- 要初始化C L H佇列,會建立一個哨兵節點,head與tail都指向哨兵節點
第二次迴圈
- 當前執行緒節點的前驅節點指向尾部節點(哨兵節點)
- 設定當前執行緒節點為尾部,tail指向當前執行緒節點
- 前尾部節點的後驅節點指向當前執行緒節點(當前尾部節點)
最後結合addWaiter與enq函式,整體看一下入隊流程圖:
出隊
CLH
佇列中的節點都是獲取資源失敗的執行緒節點,當持有資源的執行緒釋放資源時,會將head.next
指向的執行緒節點喚醒(CLH
佇列的第二個節點),如果喚醒的執行緒節點獲取資源成功,執行緒節點清空資訊設定為頭部節點(新哨兵節點),原頭部節點出隊(原哨兵節點)acquireQueued函式中的部分程式碼
//1.獲取前驅節點
final Node p = node.predecessor();
//如果前驅節點是首節點,獲取資源(子類實現)
if (p == head && tryAcquire(arg)) {
//2.獲取資源成功,設定當前節點為頭節點,清空當前節點的資訊,把當前節點變成哨兵節點
setHead(node);
//3.原來首節點下個節點指向為null
p.next = null; // help GC
//4.非異常狀態,防止指向finally邏輯
failed = false;
//5.返回執行緒中斷狀態
return interrupted;
}
private void setHead(Node node) {
//節點設定為頭部
head = node;
//清空執行緒
node.thread = null;
//清空前驅節點
node.prev = null;
}
只需要關注1~3
步驟即可,過程非常簡單,假設獲取資源成功,更換頭部節點,並把頭部節點的資訊清除變成哨兵節點,注意這個過程是不需要使用CAS
來保證,因為只有一個執行緒能夠成功獲取到資源。