## 34 只求問倒:連環相扣系列鎖面試題
## 引導語
面試中,問鎖主要是兩方面:鎖的日常使用場景 + 鎖原理,鎖的日常使用場景主要考察對鎖 API 的使用熟練度,看看你是否真的使用過這些 API,而不是紙上談兵,鎖原理主要就是問 AQS 底層的源碼原理了,如果問得更加深入的話,可能會現場讓你實現一個簡單的鎖,簡單要求的會讓你直接使用 AQS API,復雜要求的可能需要重新實現 AQS。
接下來我們一起看一看關于鎖的常見源碼面試題。
### 1 AQS 相關面試題
#### 1.1 說說自己對 AQS 的理解?
答:回答這樣的問題的時候,面試官主要考察的是你對 AQS 的知識有沒有系統的整理,建議回答的方向是由大到小,由全到細,由使用到原理。
如果和面試官面對面的話,可以邊說邊畫出我們在 AQS 源碼解析上中畫出的整體架構圖,并且可以這么說:
1. AQS 是一個鎖框架,它定義了鎖的實現機制,并開放出擴展的地方,讓子類去實現,比如我們在 lock 的時候,AQS 開放出 state 字段,讓子類可以根據 state 字段來決定是否能夠獲得鎖,對于獲取不到鎖的線程 AQS 會自動進行管理,無需子類鎖關心,這就是 lock 時鎖的內部機制,封裝的很好,又暴露出子類鎖需要擴展的地方;
2. AQS 底層是由同步隊列 + 條件隊列聯手組成,同步隊列管理著獲取不到鎖的線程的排隊和釋放,條件隊列是在一定場景下,對同步隊列的補充,比如獲得鎖的線程從空隊列中拿數據,肯定是拿不到數據的,這時候條件隊列就會管理該線程,使該線程阻塞;
3. AQS 圍繞兩個隊列,提供了四大場景,分別是:獲得鎖、釋放鎖、條件隊列的阻塞,條件隊列的喚醒,分別對應著 AQS 架構圖中的四種顏色的線的走向。
以上三點都是 AQS 全局方面的描述,接著你可以問問面試官要不要說細一點,可以的話,按照 AQS 源碼解析上下兩篇,把四大場景都說一下就好了。
這樣說的好處是很多的:
1. 面試的主動權把握在自己手里,而且都是自己掌握的知識點;
2. 由全到細的把 AQS 全部說完,會給面試官一種你對 AQS 了如指掌的感覺,再加上全部說完耗時會很久,面試時間又很有限,面試官就不會再問關于 AQS 一些刁鉆的問題了,這樣 AQS 就可以輕松過關。
當然如果你對 AQS 了解的不是很深,那么就大概回答下 AQS 的大體架構就好了,就不要說的特別細,免得給自己挖坑。
#### 1.2 多個線程通過鎖請求共享資源,獲取不到鎖的線程怎么辦?
答:加鎖(排它鎖)主要分為以下四步:
1. 嘗試獲得鎖,獲得鎖了直接返回,獲取不到鎖的走到 2;
2. 用 Node 封裝當前線程,追加到同步隊列的隊尾,追加到隊尾時,又有兩步,如 3 和 4;
3. 自旋 + CAS 保證前一個節點的狀態置為 signal;
4. 阻塞自己,使當前線程進入等待狀態。
獲取不到鎖的線程會進行 2、3、4 步,最終會陷入等待狀態,這個描述的是排它鎖。
#### 1.3 問題 1.2 中,排它鎖和共享鎖的處理機制是一樣的么?
答:排它鎖和共享鎖在問題 1.2 中的 2、3、4 步驟都是一樣的, 不同的是在于第一步,線程獲得排它鎖的時候,僅僅把自己設置為同步隊列的頭節點即可,但如果是共享鎖的話,還會去喚醒自己的后續節點,一起來獲得該鎖。
#### 1.4 共享鎖和排它鎖的區別?
答:排它鎖的意思是同一時刻,只能有一個線程可以獲得鎖,也只能有一個線程可以釋放鎖。
共享鎖可以允許多個線程獲得同一個鎖,并且可以設置獲取鎖的線程數量,共享鎖之所以能夠做到這些,是因為線程一旦獲得共享鎖,把自己設置成同步隊列的頭節點后,會自動的去釋放頭節點后等待獲取共享鎖的節點,讓這些等待節點也一起來獲得共享鎖,而排它鎖就不會這么干。
#### 1.5 排它鎖和共享鎖說的是加鎖時的策略,那么鎖釋放時有排它鎖和共享鎖的策略么?
答:是的,排它鎖和共享鎖,主要體現在加鎖時,多個線程能否獲得同一個鎖。
但在鎖釋放時,是沒有排它鎖和共享鎖的概念和策略的,概念僅僅針對鎖獲取。
#### 1.6 描述下同步隊列?
答:同步隊列底層的數據結構就是雙向的鏈表,節點叫做 Node,頭節點叫做 head,尾節點叫做 tail,節點和節點間的前后指向分別叫做 prev、next,如果是面對面面試的話,可以畫一下 AQS 整體架構圖中的同步隊列。
同步隊列的作用:阻塞獲取不到鎖的線程,并在適當時機釋放這些線程。
實現的大致過程:當多個線程都來請求鎖時,某一時刻有且只有一個線程能夠獲得鎖(排它鎖),那么剩余獲取不到鎖的線程,都會到同步隊列中去排隊并阻塞自己,當有線程主動釋放鎖時,就會從同步隊列中頭節點開始釋放一個排隊的線程,讓線程重新去競爭鎖。
#### 1.7 描述下線程入、出同步隊列的時機和過程?
答:(排它鎖為例)從 AQS 整體架構圖中,可以看出同步隊列入隊和出隊都是有兩個箭頭指向,所以入隊和出隊的時機各有兩個。
同步隊列入隊時機:
1. 多個線程請求鎖,獲取不到鎖的線程需要到同步隊列中排隊阻塞;
2. 條件隊列中的節點被喚醒,會從條件隊列中轉移到同步隊列中來。
同步隊列出隊時機:
1. 鎖釋放時,頭節點出隊;
2. 獲得鎖的線程,進入條件隊列時,會釋放鎖,同步隊列頭節點開始競爭鎖。
四個時機的過程可以參考 AQS 源碼解析,1 參考 acquire 方法執行過程,2 參考 signal 方法,3 參考 release 方法,4 參考 await 方法。
#### 1.8 為什么 AQS 有了同步隊列之后,還需要條件隊列?
答:的確,一般情況下,我們只需要有同步隊列就好了,但在上鎖后,需要操作隊列的場景下,一個同步隊列就搞不定了,需要條件隊列進行功能補充,比如當隊列滿時,執行 put 操作的線程會進入條件隊列等待,當隊列空時,執行 take 操作的線程也會進入條件隊列中等待,從一定程度上來看,條件隊列是對同步隊列的場景功能補充。
#### 1.9 描述一下條件隊列中的元素入隊和出隊的時機和過程?
答:入隊時機:執行 await 方法時,當前線程會釋放鎖,并進入到條件隊列。
出隊時機:執行 signal、signalAll 方法時,節點會從條件隊列中轉移到同步隊列中。
具體的執行過程,可以參考源碼解析中 await 和 signal 方法。
#### 1.10 描述一下條件隊列中的節點轉移到同步隊列中去的時機和過程?
答:時機:當有線程執行 signal、signalAll 方法時,從條件隊列的頭節點開始,轉移到同步隊列中去。
過程主要是以下幾步:
1. 找到條件隊列的頭節點,頭節點 next 屬性置為 null,從條件隊列中移除了;
2. 頭節點追加到同步隊列的隊尾;
3. 頭節點狀態(waitStatus)從 CONDITION 修改成 0(初始化狀態);
4. 將節點的前一個節點狀態置為 SIGNAL。
#### 1.11 線程入條件隊列時,為什么需要釋放持有的鎖?
答:原因很簡單,如果當前線程不釋放鎖,一旦跑去條件隊里中阻塞了,后續所有的線程都無法獲得鎖,正確的場景應該是:當前線程釋放鎖,到條件隊列中去阻塞后,其他線程仍然可以獲得當前鎖。
### 2 AQS 子類鎖面試題
#### 2.1 你在工作中如何使用鎖的,寫一個看一看?
答:這個照實說就好了,具體 demo 可以參考:demo.sixth.ConditionDemo。
#### 2.1 如果我要自定義鎖,大概的實現思路是什么樣子的?
答:現在有很多類似的問題,比如讓你自定義隊列,自定義鎖等等,面試官其實并不是想讓我們重新造一個輪子,而是想考察一下我們對于隊列、鎖理解的深度,我們只需要選擇自己最熟悉的 API 描述一下就好了,所以這題我們可以選擇 ReentrantLock 來描述一下實現思路:
1. 新建內部類繼承 AQS,并實現 AQS 的 tryAcquire 和 tryRelease 兩個方法,在 tryAcquire 方法里面實現控制能否獲取鎖,比如當同步器狀態 state 是 0 時,即可獲得鎖,在 tryRelease 方法里面控制能否釋放鎖,比如將同步器狀態遞減到 0 時,即可釋放鎖;
2. 對外提供 lock、release 兩個方法,lock 表示獲得鎖的方法,底層調用 AQS 的 acquire 方法,release 表示釋放鎖的方法,底層調用 AQS 的 release 方法。
#### 2.2 描述 ReentrantLock 兩大特性:可重入性和公平性?底層分別如何實現的?
答:可重入性說的是線程可以對共享資源重復加鎖,對應的,釋放時也可以重復釋放,對于 ReentrantLock 來說,在獲得鎖的時候,state 會加 1,重復獲得鎖時,不斷的對 state 進行遞增即可,比如目前 state 是 4,表示線程已經對共享資源加鎖了 4 次,線程每次釋放共享資源的鎖時,state 就會遞減 1,直到遞減到 0 時,才算真正釋放掉共享資源。
公平性和非公平指的是同步隊列中的線程得到鎖的機制,如果同步隊列中的線程按照阻塞的順序得到鎖,我們稱之為公平的,反之是非公平的,公平的底層實現是 ReentrantLock 的 tryAcquire 方法(調用的是 AQS 的 hasQueuedPredecessors 方法)里面實現的,要釋放同步隊列的節點時(或者獲得鎖時),判斷當前線程節點是不是同步隊列的頭節點的后一個節點,如果是就釋放,不是則不能釋放,通過這種機制,保證同步隊列中的線程得到鎖時,是按照從頭到尾的順序的。
#### 2.3 如果一個線程需要等待一組線程全部執行完之后再繼續執行,有什么好的辦法么?是如何實現的?
答:CountDownLatch 就提供了這樣的機制,比如一組線程有 5 個,只需要在初始化 CountDownLatch 時,給同步器的 state 賦值為 5,主線程執行 CountDownLatch.await ,子線程都執行 CountDownLatch.countDown 即可。
#### 2.4 Atomic 原子操作類可以保證線程安全,如果操作的對象是自定義的類的話,要如何做呢?
答: Java 為這種情況提供了一個 API:AtomicReference,AtomicReference 類可操作的對象是個泛型,所以支持自定義類。
### 3 總結
關于 AQS 和鎖場景的面試題,其實網上也很多,各個大廠出的題目也都不一樣,但考察問題的本質都是一致的,如果把 AQS 架構圖中,AQS 的組成和四種顏色箭頭的發起時機,調用過程都弄清楚了,回答 AQS 的各種問題都會游刃有余。
- 前言
- 第1章 基礎
- 01 開篇詞:為什么學習本專欄
- 02 String、Long 源碼解析和面試題
- 03 Java 常用關鍵字理解
- 04 Arrays、Collections、Objects 常用方法源碼解析
- 第2章 集合
- 05 ArrayList 源碼解析和設計思路
- 06 LinkedList 源碼解析
- 07 List 源碼會問哪些面試題
- 08 HashMap 源碼解析
- 09 TreeMap 和 LinkedHashMap 核心源碼解析
- 10 Map源碼會問哪些面試題
- 11 HashSet、TreeSet 源碼解析
- 12 彰顯細節:看集合源碼對我們實際工作的幫助和應用
- 13 差異對比:集合在 Java 7 和 8 有何不同和改進
- 14 簡化工作:Guava Lists Maps 實際工作運用和源碼
- 第3章 并發集合類
- 15 CopyOnWriteArrayList 源碼解析和設計思路
- 16 ConcurrentHashMap 源碼解析和設計思路
- 17 并發 List、Map源碼面試題
- 18 場景集合:并發 List、Map的應用場景
- 第4章 隊列
- 19 LinkedBlockingQueue 源碼解析
- 20 SynchronousQueue 源碼解析
- 21 DelayQueue 源碼解析
- 22 ArrayBlockingQueue 源碼解析
- 23 隊列在源碼方面的面試題
- 24 舉一反三:隊列在 Java 其它源碼中的應用
- 25 整體設計:隊列設計思想、工作中使用場景
- 26 驚嘆面試官:由淺入深手寫隊列
- 第5章 線程
- 27 Thread 源碼解析
- 28 Future、ExecutorService 源碼解析
- 29 押寶線程源碼面試題
- 第6章 鎖
- 30 AbstractQueuedSynchronizer 源碼解析(上)
- 31 AbstractQueuedSynchronizer 源碼解析(下)
- 32 ReentrantLock 源碼解析
- 33 CountDownLatch、Atomic 等其它源碼解析
- 34 只求問倒:連環相扣系列鎖面試題
- 35 經驗總結:各種鎖在工作中使用場景和細節
- 36 從容不迫:重寫鎖的設計結構和細節
- 第7章 線程池
- 37 ThreadPoolExecutor 源碼解析
- 38 線程池源碼面試題
- 39 經驗總結:不同場景,如何使用線程池
- 40 打動面試官:線程池流程編排中的運用實戰
- 第8章 Lambda 流
- 41 突破難點:如何看 Lambda 源碼
- 42 常用的 Lambda 表達式使用場景解析和應用
- 第9章 其他
- 43 ThreadLocal 源碼解析
- 44 場景實戰:ThreadLocal 在上下文傳值場景下的實踐
- 45 Socket 源碼及面試題
- 46 ServerSocket 源碼及面試題
- 47 工作實戰:Socket 結合線程池的使用
- 第10章 專欄總結
- 48 一起看過的 Java 源碼和面試真題