兄得我今天分享一次有關AQS的面試經曆,如有雷同,咱們可能是同一個面試官,瓜子、闆凳、啤酒、花生米備好了,咱們正式開始。
你能說一說什麼是AQS嗎?
AQS是隊列同步器AbstractQueueSynchronizer的簡寫,它是用來建構鎖和其他同步元件的基礎架構,它定義了一個全局的int 型的state變量,通過内置的FIFO(先進先出)隊列來完成資源競争排隊的工作。
AQS中使用到了哪些設計模式?
模版設計模式
如何修改和通路同步器的狀态?
- getState:擷取目前同步狀态
- setState:設定目前同步狀态
- compareAndSetState:使用CAS設定目前狀态,該方法能保證狀态設定的原子性。
AQS提供了哪些模版方法?
方法名稱 | 方法說明 |
---|---|
Acquire() | 獨占鎖擷取同步狀态,如果目前線程擷取同步狀态成功,則由該方法傳回,否則,将會進入同步隊列等待,該方法将會調用重寫的tryAccquire()方法 |
acquireShared() | 擷取共享鎖,如果目前線程沒有擷取到共享鎖,則會進入到同步等待隊列,,他與獨占鎖不同的是,共享鎖能被多個線程同時占有 |
release | 釋放同步狀态,并且通知同步器,喚醒等待隊列中第一個節點中包含的線程。 |
releaseShare | 釋放同步狀态 |
上面總共列出了7個模版方法,可以将這7個模版方法歸為如下三類:獨占鎖的擷取與釋放、共享鎖的擷取與釋放、同步狀态的查詢與設定
你能說下AQS中同步隊列的資料結構麼?
内心OS:幸好我提前刷了面試題,不然就被卡在這裡了。首先來張圖,來鎮鎮場子
- 目前線程擷取同步狀态失敗,同步器将目前線程機等待狀态等資訊構造成一個Node節點加入隊列,放在隊尾,同步器重新設定尾節點
- 加入隊列後,會阻塞目前線程
- 同步狀态被釋放并且同步器重新設定首節點,同步器喚醒等待隊列中第一個節點,讓其再次擷取同步狀态
上面的這個流程,細心的同學應該會發現一個問題,設定首尾節點的時候會不會發生線程安全問題呢?如果會的話應該怎麼做呢?
接下來,我們看下它們是怎麼做的:
同步器如何設定尾節點,才能保證線程安全呢?
我們先分析下,為什麼設定尾節點的時候會出現線程安全呢?
- 當多個線程同一時刻去擷取同步狀态(獨占鎖)的時候,肯定隻會有一個線程競争成功,那麼其他線程都會被放到等待隊列的末尾,當多個線程同時被塞到隊列末尾的時候,就相當于同時競争末尾這個資源,這時候就會出現線程安全問題了
- 為了保證設定隊尾元素線程安全,同步器提供了compareAndSetTail(Node expecr,Node update)方法,他傳入目前線程認為的尾節點和目前要設定成尾節點的節點,隻有設定成功,才将目前節點正式與之前的尾節點建立關聯。
同步器如何設定首節點的時候,是不是也要用cas來保證線程安全呢?
“面試官臉上的笑容要多邪媚有多邪魅“
當時第一反應,當然咯,但是在大腦飛速運轉之後,回想起來昨天晚上刷面試題的時候,好像刷到了這個題,然後推口而出:當然不會咯,面試官的笑容逐漸消失,當即問我,為什麼不會?我是這樣回答的:
- 首先 同步器設定尾節點的時候需要cas保證線程安全性是因為設定尾節點的時候是存在多個線程同時競争設定的,但是設定尾節點的時候是不會存在多個線程同時競争去設定的。
- 因為,釋放同步狀态設定頭節點的時候,隻有擷取到同步狀态的線程才能設定,能夠擷取到獨占鎖的同步狀态,當然隻有一個線程啦,是以這裡是不可能發生線程安全性的問題,那麼就不需要使用CAS保證線程安全的問題了,直接斷開之前的首節點,将下一個節點設定成首節點
獨占式同步狀态的擷取與釋放的源碼你有了解麼?
“我内心的獨白是,這個我早已了然于胸”
public final void acquire(int arg) {
if(!tryAcquire(arg)
&&acquireQueued(addWaiter(Node.EXCLUSIVE),arg))
seleInterrupt();
}
}
複制
哈哈,雖然隻有幾行代碼,但是它卻完成了同步鎖擷取的整個過程,你還别不信,我來和你娓娓道來
- 目前線程調用自定義同步器實作的tryAcquire方法,該方法保證線程安全的擷取同步狀态,如果同步狀态擷取失敗,則執行後續流程
- 構造獨占式同步節點,通過調用addWaiter方法将Node塞入同步隊列尾部,并且調用acquireQueued方法自旋擷取同步鎖狀态,如果擷取不到,則阻塞目前線程。
private Node addWaiter(Node node) {
Node node = new Node(Thread.currentThread(),mode);
// 快速嘗試在尾部添加
Node pred = tail;
if(pred!=null){
node.prev = pred;
if(compareAndSetTail(pred,node)) {
pred.next = node;
return node;
}
}
enq(node);
}
private Node enq(final Node node) {
for(;;){
Node t = tail;
if(t==null){
if(compareAndSetHead(new Node())){
tail = node;
}
}else{
node.prev = t;
if(compareAndSetTail(t,node)){
t.next = node;
return t;
}
}
複制
上面兩段代碼第一次看會有點暈,樂哉也是的,當時結合上面分析過的流程來看,感覺就會清晰很多,我直接畫個思維導圖吧
我們歇一會,繼續分析acquireQueued 方法,看看它裡面都做了什麼呢?
final boolean acquireQueued(final Node node,int arg){
boolean failed = true;
try{
boolean interrupted = false;
for(;;){
final Node p = node.predcessor();
if(p == head && tryAcquire(arg)) {
setHead(node);
p.next = null;
failed = false;
return interrupted;
}
}
}
}
複制
分析上面的代碼片段,可以得出acquireQueued内部使用自旋的方式擷取同步狀态,并且隻有 目前節點的前驅節點是頭節點,才會嘗試調用tryAcquire 擷取同步狀态,否則繼續自旋。面試官看到我說到了這裡,就又插了一句說:
為什麼隻能其前驅節點是頭節點,才會嘗試擷取同步狀态呢?
真想來一句,這不快要說了麼!!(暴脾氣)
為什麼隻能其前驅節點是頭節點,才會嘗試擷取同步
當擁有同步狀态的線程釋放同步狀态的時候,會喚醒其後繼節點。為了維護FIFO原則,其後繼節點被喚醒後需要檢查自己的前驅節點是不是頭節點,這樣才能保證FIFO原則。獨占鎖的釋放我就不說了,這裡用一個流程圖來彙總下我說内容
你前面一直在說獨占鎖和共享鎖,你來說說他們之間有什麼差別?
老師,我給你畫個圖吧,畫畫個北北,畫畫個北北。。。突然唱起來了,面試官一臉瞪着我,好尴尬啊。
面試官回答說:這個是他們概念上的差別
你能說說AQS在代碼實作,這兩種的差別麼?
從面試官的回答來看,他其實就是想試探下你有沒有深入的去了解AQS的源碼,因為我是看過的啊,于是我又繼續和他侃大山了。
- 差別1:共享鎖是可以被多個線程同時擁有,并且擷取同步狀态是否成功,共享鎖是通過判斷傳回值是否大于0,而獨占鎖是通過true或false來判斷
- 差別2:獨占鎖在釋放同步狀态的時候是不用關心線程安全的問題,因為隻有一個線程在釋放同步狀态,但是共享鎖是被多個線程同時擁有,是以釋放同步狀态的時候需要保證線程安全,一般通過CAS+自旋來實作