AQS底層原理與源碼解析

介紹AQS AbstractQueuedSynchronizer  抽象隊列鎖  用來構建鎖的框架

  1. state變量
    1. 使用int類型的volatile變量維護同步狀態(state)
    2. 圍繞state提供鎖的兩種操作“獲取”和“釋放=0”; 讀鎖與寫鎖區分 65535= 2^16-1
    3. FutureTask用它來表示任務的狀態
  2. 內置的同步隊列 CLH,雙端雙向列表
    1. FIFO隊列存放阻塞的等待線程,來完成線程的排隊執行;
    2. 封裝成Node,Node維護一個prev引用和next引用,實現雙向鏈表
    3. AQS維護兩個指針,分別指向隊列頭部head和尾部tail
  3. 功能:
  4.  

鎖是面向使用者的,定義了用戶調用的接口,隱藏了實現細節;

AQS是鎖的實現者,屏蔽了同步狀態管理,線程的排隊,等待喚醒的底層操作。

鎖是面向使用者,AQS是鎖的具體實現者

背後複雜的線程排隊,線程阻塞/喚醒,如何保證線程安全,都由AQS爲我們完成了

分爲獨佔鎖、共享鎖

繼承AbstractQueuedSynchronizer並重寫指定的方法

獨佔式如ReentrantLock,共享式如Semaphore,CountDownLatch

 

1.要實現一個獨佔鎖,那就去重寫或封裝tryAcquire,tryRelease方法,

Acquire:tryAcquire(arg)、addWaiter【入隊】、acquireQueued【循環獲取鎖,失敗則掛起shouldParkAfterFailedAcquire】

Release:tryRelease(arg)、檢查waitStatus、unparkSuccessor(h)【;//喚醒後繼結點】

 

2.要實現共享鎖,就去重寫tryAcquireShared,tryReleaseShared

acquireShared:tryAcquireShared、doAcquireShared(arg)【setHeadAndPropagate】;【嘗試獲取(state+1),入隊、等待喚醒】

realseShared:tryRealseShared、doReleaseShared()【CAS下的 unparkSuccessor(h)】;【state-1並判斷、喚醒後繼】

 

等待狀態位(waitStatus)【等待模式】

CANCELLED 1:因爲超時或者中斷,結點被設置爲取消狀態,不能去競爭鎖,不能轉換爲其他狀態;會被檢測並踢出隊列,被GC回收;

SIGNAL -1:表示這個結點的繼任結點被阻塞,到時需要喚醒它

CONDITION -2:表示這個結點在條件隊列中,因爲等待某個條件而被阻塞;

共享傳播狀態

PROPAGATE -3:使用在共享模式頭結點有可能牌處於這種狀態,表示鎖的下一次獲取可以無條件傳播;

 

閉鎖(CountDownLatch) 源碼,是一種共享鎖

N個線程調用await阻塞在for循環裏面,然後N個線程依次調用countDown,每調用一次state減1,直接state爲0,這些線程退出for循環(解除阻塞)!

退出for循環時,由於頭結點狀態標誌位爲PROPAGATE,而且這些結點都是共享模式,由頭結點一傳播,這些結點都獲取鎖,於是齊頭並進執行了......

 

  1. public void await() throws InterruptedException;
  2. public void countDown();

 

countDown()方法調用 releaseShared(int arg),直到state-1後爲0.開始執行傳遞喚醒後繼shared線程

void countDown{

if (tryReleaseShared(arg)) { // state-1後是否爲 0

doReleaseShared(); //檢測共享節點和等待狀態爲 PROPAGATE -3的後繼節點,開始喚醒unpark(thread);

return true;

}

}

 

await方法使當前線程一直等待,阻塞在doAcquireShared方法中,除非線程被中斷,或者 state == 0;

當調用countDown方法之後state-1,當鎖存器減少到0時,await方法就會返回

 

 

兩次doReleaseShared();的區別

  1. setHeadAndPropagate()方法表示等待隊列中的線程成功獲取到共享鎖,這時候它需要喚醒它後面的共享節點的線程
  2. 但是當通過releaseShared()方法去釋放一個共享鎖的時,接下來等待獨佔鎖跟共享鎖的線程都可以被喚醒進行嘗試獲取鎖

 

 

獨佔鎖

Acquire:tryAcquire(arg)、addWaiter【線程入隊】、acquireQueued【循環獲取鎖,失敗則掛起shouldParkAfterFailedAcquire,等待頭結點喚醒】

Release:tryRelease(arg)【每次state-1】、unparkSuccessor(h)【若state==0,喚醒後繼結點】

獲取acquire(int arg)

 

a.首先,調用使用者重寫的tryAcquire方法,若返回true,意味着獲取同步狀態成功,後面的邏輯不再執行;若返回false,也就是獲取同步狀態失敗,進入b步驟;

b.此時,獲取同步狀態失敗,構造獨佔式同步結點,通過addWatiter將此結點添加到同步隊列的尾部(此時可能會有多個線程結點試圖加入同步隊列尾部,需要以線程安全的方式添加);

c.該結點以在隊列中嘗試獲取同步狀態,若獲取不到,則阻塞結點線程,直到被前驅結點喚醒或者被中斷

 

public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt();//請求鎖成功,加入等待隊列,中斷自己,等待被喚醒 }

 

tryAcquire(arg)會調用nonfairTryAcquire(1),含插隊

獲取當前線程

獲取並檢查鎖狀態,若c==0,CAS將state置爲1,並設置當前線程獲取獨佔鎖,返回true

else,查看當前線程是不是已經是獨佔鎖。若是,state+1,返回true

都不是,返回false

final boolean nonfairTryAcquire(int acquires) { final Thread current = Thread.currentThread();//獲取當前線程 int c = getState();//獲取鎖數量 if (c == 0) {//如果鎖數量爲0,證明該獨佔鎖已被釋放,當下沒有線程在使用 if (compareAndSetState(0, acquires)) {//繼續通過CAS將state由0變爲1,注意這裏傳入的acquires爲1; setExclusiveOwnerThread(current);//將當前線程設置爲獨佔鎖的線程 return true; } } else if (current == getExclusiveOwnerThread()) {//查看當前線程是不是就是獨佔鎖的線程 int nextc = c + acquires;//如果是,鎖狀態的數量爲當前的鎖數量+1 if (nextc < 0) // overflow throw new Error("Maximum lock count exceeded"); setState(nextc);//設置當前的鎖數量 return true; } return false; }

若請求鎖失敗,addWaiter 將當前線程鏈入隊尾並掛起,之後等待被喚醒 【快速、正常】

  1. 首先使用addWaiter(Node.EXCLUSIVE)將當前線程封裝進Node節點,然後將該節點加入等待隊列
    1. 先快速入隊:存在尾節點,將使用CAS嘗試將尾節點設置爲node】
    2. 如果快速入隊不成功【尾節點爲空】,使用正常入隊方法enq,【無限循環=第一次阻塞】:創建一個dummy節點,並將該節點通過CAS設置到頭節點】【期間多線程:若頭結點不爲null,執行快速入隊到尾節點】

acquireQueued 入隊成功後,返回node節點,繼續第三次插隊

無限循環調用:獲取node的前驅節點p

p==head&&tryAcquire(1) 是唯一跳出循環的方法:p成爲頭結點並且獲取鎖成功:如果p是頭節點,就繼續使用tryAcquire(1)方法插隊,若成功,不用中斷,第三次插隊成功;【只有前驅結點是頭結點的結點,也就是老二結點,纔有機會去tryAcquire;】

  1. 如果p不是頭節點,或者tryAcquire(1)請求不成功,執行shouldParkAfterFailedAcquire(Node pred, Node node)來檢測當前節點是不是可以安全的被掛起:判斷前驅節點p的等待狀態waitStatus
    1. SIGNAL(即可以喚醒下一個節點的線程),則node節點的線程可以安全掛起,返回true
    2. CANCELLED,則p的線程被取消了,我們會將p之前的連續幾個被取消的前驅節點從隊列中剔除
    3. 等待狀態是除了上述兩種的其他狀態,CAS嘗試將前驅節點的等待狀態設爲SIGNAL【p與node競爭】,返回true

 

掛起後後 跳出循環,需要中斷自身

LockSupport.park(this);return Thread.interrupted(); //掛起當前的線程,後等待前驅節點unpark喚醒該線程;喚醒方法爲public

 

釋放 release(int arg)

 

//釋放鎖的操作

public final boolean release(int arg)

//嘗試釋放鎖,若釋放後state==0,成功,喚醒後繼節點;否則失敗直接返回false;若成功,判斷waitStatus不等於0,喚醒後繼節點 if (tryRelease(arg)) { Node h = head; if (h != null && h.waitStatus != 0) unparkSuccessor(h); return true; } return false; }

 

調用tryRelease釋放鎖,如果釋放失敗,直接返回

protected final boolean tryRelease(int releases) {

//獲取state值,釋放一定值 int c = getState() - releases; if (Thread.currentThread() != getExclusiveOwnerThread()) throw new IllegalMonitorStateException(); boolean free = false;

//如果差是0,表示鎖已經完全釋放 if (c == 0) { free = true;

//下面設置爲null表示當前沒有線程佔用鎖 setExclusiveOwnerThread(null); }

//如果c不是0表示鎖還沒有完全釋放,修改state值 setState(c); return free; }

 

釋放鎖成功後需要喚醒繼任結點,是通過方法unparkSuccessor實現

若後繼結點爲空或處於CANCEL狀態,從後向前遍歷找尋一個正常的結點,喚醒其對應線程

private void unparkSuccessor(Node node) { //傳進來頭結點

int ws = node.waitStatus;

if (ws < 0)//檢查頭結點的waitStatus位,小於0表示沒被取消

compareAndSetWaitStatus(node, ws, 0);//將當前節點的狀態修改爲0

 

Node s = node.next;

if (s == null || s.waitStatus > 0) {

s = null;

//從尾向頭尋找,tail開始,尋找pre

for (Node t = tail; t != null && t != node; t = t.prev)

if (t.waitStatus <= 0)

s = t;

}

//查看頭結點的下一個結點,如果下一個結點不爲空,且waitStatus<=0,表示後繼結點沒有被取消

if (s != null)

LockSupport.unpark(s.thread);

}

 

這裏並沒有從頭向尾尋找,而是尾向頭尋找,爲什麼

因爲在CLH隊列中的結點隨時有可能被中斷,被中斷的結點的waitStatus設置爲CANCEL,而且它會被踢出CLH隊列,如何個踢出法,就是它的前趨結點的next並不會指向它,而是指向下一個非CANCEL的結點,而它自己的next指針指向它自己。一旦這種情況發生,如何從頭向尾方向尋找繼任結點會出現問題,因爲一個CANCEL結點的next爲自己,那麼就找不到正確的繼任接點

CANCEL結點的next指針爲什麼要指向它自己,爲什麼不指向真正的next結點?爲什麼不爲NULL?

第一個問題的答案是這種被CANCEL的結點最終會被GC回收,如果指向next結點,GC無法回收

第二個問題的回答,爲了使isOnSyncQueue方法更新簡單,判斷一個結點是否在同步隊列中,指向它自己時表示在,可以踢出

如果一個結點next不爲空,那麼它在同步隊列中,如果CANCEL結點的後繼爲空那麼CANCEL結點不在同步隊列中,這與事實相矛盾。

因此將CANCEL結點的後繼指向它自己是合理的選擇。

 

共享鎖:

acquireShared:tryAcquireShared、doAcquireShared(arg)【setHeadAndPropagate】;

realseShared:tryRealseShared、doReleaseShared()【unparkSuccessor(h)】;

 

  1. 共享式:共享式地獲取同步狀態
  2. 對於獨佔式同步組件來講,同一時刻只有一個線程能獲取到同步狀態,其他線程都得去排隊等待,其待重寫的嘗試獲取同步狀態的方法tryAcquire返回值爲boolean,這很容易理解;
  3. 對於共享式同步組件來講,同一時刻可以有多個線程同時獲取到同步狀態,這也是“共享”的意義所在。其待重寫的嘗試獲取同步狀態的方法tryAcquireShared返回值爲int。

lock,調用acquireShared

public void lock() {

sync.acquireShared(1);

}

//獲取共享鎖API acquireShared

public final void acquireShared(int arg) {

//state != 0時,tryAcquireShared(arg) < 0,纔會真正操作鎖;表示獲取鎖失敗

if (tryAcquireShared(arg) < 0)

doAcquireShared(arg);

}

 

tryAcquireShared(arg) 判斷是否需要喚醒後續節點獲取共享鎖

tryAcquireShared(arg):return (getState() == 0) ? 1 : -1;返回state是否爲0

protected int tryAcquireShared(int acquires) { return (getState() == 0) ? 1 : -1; }

< 0:表示state != 0,獲取鎖失敗,執行doAcquireShared(arg)

//= 0:表示當前線程獲取共享鎖成功,但不需要把它後面等待的節點喚醒

> 0:state==0,表示當前線程獲取共享鎖成功,且此時需要把後續節點喚醒讓它們去嘗試獲取共享鎖

獲取鎖失敗後 操作doAcquireShared(int arg) ,裏面繼續循環嘗試獲取共享鎖,若成功,可以傳播狀態;失敗則入隊掛起

大體邏輯與獨佔式的acquireQueued差距不大,只不過由於是共享式,會有多個線程同時獲取到線程,也可能同時釋放線程,空出很多同步狀態,

所以當排隊中的老二獲取到同步狀態,如果還有可用資源,會繼續傳播下去。

 

  1. 封裝爲Node.SHARED,調用addWaiter:根據隊列是否爲空採用CAS進行快速入隊和正常入隊,返回頭結點
  2. 自旋,檢測是否前繼節點p爲head,是則再次調用tryAcquireShared(arg),嘗試獲取,若state==0,
  3. 調用setHeadAndPropagate,根據s.isShared()標識,調用doReleaseShared()
    1. 作用:當排隊中的老二獲取到同步狀態,如果還有可用資源[waitStatusj和共享標識],會繼續傳播下去

 

釋放共享鎖ReleaseShared(int arg)

tryReleaseShared(arg):釋放共享鎖,state-1

state-1後,返回是否爲0;相當於一個柵欄,只有當state==0,繼續執行doReleaseShared()

if (tryReleaseShared(arg)) {//state爲0時,返回true(針對CountDownLatch)

doReleaseShared();

return true;

}

doReleaseShared(); 調用unparkSuccessor(h);傳遞喚醒阻塞的共享線程

死循環,共享模式,持有同步狀態的線程可能有多個,採用循環CAS保證線程安全

釋放同步狀態也是多線程的,此處採用了CAS自旋來保證

private void doReleaseShared() { for (;;) {//死循環,共享模式,持有同步狀態的線程可能有多個,採用循環CAS保證線程安全 Node h = head; if (h != null && h != tail) { int ws = h.waitStatus; if (ws == Node.SIGNAL) { if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0)) continue; unparkSuccessor(h);//喚醒後繼結點 } else if (ws == 0 && !compareAndSetWaitStatus(h, 0, Node.PROPAGATE)) continue; } if (h == head) break; } }

 

1)共享鎖初始化時會給state設值,所有請求鎖的共享節點都會放入SyncQueue中阻塞

2)一個節點A獲取鎖(成爲head節點)之後,會喚醒它的下一個共享節點線程B,B喚醒後會去競爭鎖,一直往下,直到後面的共享節點都喚醒爲止。

此時所有共享節點都獲取了鎖,都可以往下執行了

 

如果state值代表的許可數足夠使用,那麼請求線程將會獲得同步狀態即對共享資源的訪問權,並更新state的值(一般是對state值減1),但如果state值代表的許可數已爲0,則請求線程將無法獲取同步狀態,線程將被加入到同步隊列並阻塞,直到其他線程釋放同步狀態(一般是對state值加1)纔可能獲取對共享資源的訪問權

 

acquireShared()方法獲取鎖

  1. 當tryAcquireShared(arg)返回值>=0時(可以在重寫該方法時自定義鎖的數量),表示獲取鎖成功,不會進入doAcquireShared。
  2. 當tryAcquireShared(arg)返回值<0時,線程都鎖完,等待被喚醒,進入doAcquireShared(arg)方法,封裝Node.SHARED節點放入等待隊列,並自旋阻塞

 

private void doAcquireShared(int arg) {

final Node node = addWaiter(Node.SHARED);//構造一個共享結點,添加到同步隊列尾部。若隊列初始爲空,先添加一個無意義的dummy結點,再將新節點添加到隊列尾部

boolean failed = true;//是否獲取成功

try {

boolean interrupted = false;//線程parking過程中是否被中斷過

for (;;) {//自旋

final Node p = node.predecessor();//獲取前驅結點

if (p == head) {//頭結點持有同步狀態,只有前驅是頭結點,纔有機會嘗試獲取同步狀態

int r = tryAcquireShared(arg);//嘗試獲取同步資源

if (r >= 0) {//r>=0,獲取成功

setHeadAndPropagate(node, r);//獲取成功就將當前結點設置爲頭結點,若還有可用資源,傳播下去,也就是繼續喚醒後繼結點,即doReleaseShared();

p.next = null; // 方便GC

if (interrupted)

selfInterrupt();

failed = false;

return;

}

}

if (shouldParkAfterFailedAcquire(p, node) &&//是否能安心進入parking狀態

parkAndCheckInterrupt())//阻塞線程

interrupted = true;

}

} finally {

if (failed)

cancelAcquire(node);

}

}

 

//setHeadAndPropagate 把node節點設置成head節點,且node.waitStatus->Node.PROPAGATE*

private void setHeadAndPropagate(Node node, int propagate) {

Node h = head;//h用來保存舊的head節點

setHead(node);//head引用指向node節點

/* 這裏意思有兩種情況是需要執行喚醒操作

* 1.propagate > 0 表示調用方指明瞭後繼節點需要被喚醒

* 2.頭節點後面的節點需要被喚醒(waitStatus<0),不論是老的頭結點還是新的頭結點*/

if (propagate > 0 || h == null || h.waitStatus < 0 ||

(h = head) == null || h.waitStatus < 0) {

Node s = node.next;

if (s == null || s.isShared())//node是最後一個節點或者 node的後繼節點是共享節點

/* 如果head節點狀態爲SIGNAL,喚醒head節點線程,重置head.waitStatus->0

* head節點狀態爲0(第一次添加時是0),設置head.waitStatus->Node.PROPAGATE表示狀態需要向後繼節點傳播

*/

doReleaseShared();//對於這個方法,其實就是把node節點設置成Node.PROPAGATE狀態

}

}

 

區分讀鎖和寫鎖

tryAcquire 函數是嘗試獲取寫鎖:1.如果有讀線程或者寫線程且不是當前線程,直接失敗;2.如果寫鎖的count超過了65535,直接失敗

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章