## 36 從容不迫:重寫鎖的設計結構和細節
## 引導語
有的面試官喜歡讓同學在說完鎖的原理之后,讓你重寫一個新的鎖,要求現場在白板上寫出大概的思路和代碼邏輯,這種面試題目,蠻難的,我個人覺得其側重點主要是兩個部分:
1. 考察一下你對鎖原理的理解是如何來的,如果你對源碼沒有解讀過的話,只是看看網上的文章,或者背面試題,也是能夠說出大概的原理,但你很難現場寫出一個鎖的實現代碼,除非你真的看過源碼,或者有和鎖相關的項目經驗;
2. 我們不需要創造,我們只需要模仿 Java 鎖中現有的 API 進行重寫即可。
如果你看過源碼,這道題真的很簡單,你可以挑選一個你熟悉的鎖進行模仿。
在鎖章節中我們之前說的都是排它鎖,這小節我們以共享鎖作為案列,自定義一個共享鎖。
### 1 需求
一般自定義鎖的時候,我們都是根據需求來進行定義的,不可能憑空定義出鎖來,說到共享鎖,大家可能會想到很多場景,比如說對于共享資源的讀鎖可以是共享的,比如對于數據庫鏈接的共享訪問,比如對于Socket 服務端的鏈接數是可以共享的,場景有很多,我們選擇共享訪問數據庫鏈接這個場景來定義一個鎖。
### 2 詳細設計
假定(以下設想都為假定)我們的數據庫是單機 mysql,只能承受 10 個鏈接,創建數據庫鏈接時,我們是通過最原始 JDBC 的方式,我們用一個接口把用 JDBC 創建鏈接的過程進行了封裝,這個接口我們命名為:創建鏈接接口。
共享訪問數據庫鏈接的整體要求如下:所有請求加在一起的 mysql 鏈接數,最大不能超過 10(包含 10),一旦超過 10,直接報錯。
在這個背景下,我們進行了下圖的設計:

這個設計最最關鍵的地方,就是我們通過能否獲得鎖,來決定是否可以得到 mysql 鏈接,如果能獲得鎖,那么就能得到鏈接,否則直接報錯。
接著我們一起來看下落地的代碼:
#### 2.1 定義鎖
首先我們需要定義一個鎖出來,定義時需要有兩個元素:
1. 鎖的定義:同步器 Sync;
2. 鎖對外提供的加鎖和解鎖的方法。
共享鎖的代碼實現如下:
```
// 共享不公平鎖 public class ShareLock implements Serializable{ // 同步器 private final Sync sync; // 用于確保不能超過最大值 private final int maxCount; /** * 初始化時給同步器 sync 賦值 * count 代表可以獲得共享鎖的最大值 */ public ShareLock(int count) { this.sync = new Sync(count); maxCount = count; } /** * 獲得鎖 * @return true 表示成功獲得鎖,false 表示失敗 */ public boolean lock(){ return sync.acquireByShared(1); }
/** * 釋放鎖 * @return true 表示成功釋放鎖,false 表示失敗 */ public boolean unLock(){ return sync.releaseShared(1); } }
```
從上述代碼中可以看出,加鎖和釋放鎖的實現,都依靠同步器 Sync 的底層實現。
唯一需要注意的是,鎖需要規定好 API 的規范,主要是兩方面:
1. API 需要什么,就是鎖在初始化的時候,你需要傳哪些參數給我,在 ShareLock 初始化時,需要傳最大可共享鎖的數目;
2. 需要定義自身的能力,即定義每個方法的入參和出參。在 ShareLock 的實現中,加鎖和釋放鎖的入參都沒有,是方法里面寫死的 1,表示每次方法執行,只能加鎖一次或釋放鎖一次,出參是布爾值,true 表示加鎖或釋放鎖成功,false 表示失敗,底層使用的都是 Sync 非公平鎖。
以上這種思考方式是有方法論的,就是我們在思考一個問題時,可以從兩個方面出發:API 是什么?API 有什么能力?
#### 2.2 定義同步器 Sync
Sync 直接繼承 AQS ,代碼如下:
```
class Sync extends AbstractQueuedSynchronizer { // 表示最多有 count 個共享鎖可以獲得 public Sync(int count) { setState(count);
} // 獲得 i 個鎖 public boolean acquireByShared(int i) { // 自旋保證 CAS 一定可以成功 for(;;){ if(i<=0){ return false; } int state = getState(); // 如果沒有鎖可以獲得,直接返回 false if(state <=0 ){ return false; } int expectState = state - i; // 如果要得到的鎖不夠了,直接返回 false if(expectState < 0 ){ return false; } // CAS 嘗試得到鎖,CAS 成功獲得鎖,失敗繼續 for 循環 if(compareAndSetState(state,expectState)){ return true; } } } // 釋放 i 個鎖 @Override protected boolean tryReleaseShared(int arg) { for(;;){ if(arg<=0){ return false; }
int state = getState(); int expectState = state + arg; // 超過了 int 的最大值,或者 expectState 超過了我們的最大預期 if(expectState < 0 || expectState > maxCount){ log.error("state 超過預期,當前 state is {},計算出的 state is {}",state ,expectState); return false; } if(compareAndSetState(state, expectState)){ return true; } } } }
```
整個代碼比較清晰,我們需要注意的是:
1. 邊界的判斷,比如入參是否非法,釋放鎖時,會不會出現預期的 state 非法等邊界問題,對于此類問題我們都需要加以判斷,體現出思維的嚴謹性;
2. 加鎖和釋放鎖,需要用 for 自旋 + CAS 的形式,來保證當并發加鎖或釋放鎖時,可以重試成功。寫 for 自旋時,我們需要注意在適當的時機要 return,不要造成死循環,CAS 的方法 AQS 已經提供了,不要自己寫,我們自己寫的 CAS 方法是無法保證原子性的。
#### 2.3 通過能否獲得鎖來決定能否得到鏈接
鎖定義好了,我們需要把鎖和獲取 Mysql 鏈接結合起來,我們寫了一個 Mysql 鏈接的工具類,叫MysqlConnection,其主要負責兩大功能:
1. 通過 JDBC 建立和 Mysql 的鏈接;
2. 結合鎖,來防止請求過大時,Mysql 的總鏈接數不能超過 10 個。
首先我們看下 MysqlConnection 初始化的代碼:
```
public class MysqlConnection { private final ShareLock lock; // maxConnectionSize 表示最大鏈接數 public MysqlConnection(int maxConnectionSize) { lock = new ShareLock(maxConnectionSize); } }
```
我們可以看到,在初始化時,需要制定最大的鏈接數是多少,然后把這個數值傳遞給鎖,因為最大的鏈接數就是 ShareLock 鎖的 state 值。
接著為了完成 1,我們寫了一個 private 的方法:
```
// 得到一個 mysql 鏈接,底層實現省略 private Connection getConnection(){}
```
然后我們實現 2,代碼如下:
```
// 對外獲取 mysql 鏈接的接口 // 這里不用try finally 的結構,獲得鎖實現底層不會有異常 // 即使出現未知異常,也無需釋放鎖 public Connection getLimitConnection() { if (lock.lock()) { return getConnection(); } return null; } // 對外釋放 mysql 鏈接的接口 public boolean releaseLimitConnection() { return lock.unLock(); }
```
邏輯也比較簡單,加鎖時,如果獲得了鎖,就能返回 Mysql 的鏈接,釋放鎖時,在鏈接關閉成功之后,調用 releaseLimitConnection 方法即可,此方法會把鎖的 state 狀態加一,表示鏈接被釋放了。
以上步驟,針對 Mysql 鏈接限制的場景鎖就完成了。
### 3 測試
鎖寫好了,接著我們來測試一下,我們寫了一個測試的 demo,代碼如下:
```
public static void main(String[] args) { log.info("模仿開始獲得 mysql 鏈接"); MysqlConnection mysqlConnection = new MysqlConnection(10); log.info("初始化 Mysql 鏈接最大只能獲取 10 個"); for(int i =0 ;i<12;i++){ if(null != mysqlConnection.getLimitConnection()){ log.info("獲得第{}個數據庫鏈接成功",i+1); }else { log.info("獲得第{}個數據庫鏈接失敗:數據庫連接池已滿",i+1); } } log.info("模仿開始釋放 mysql 鏈接"); for(int i =0 ;i<12;i++){ if(mysqlConnection.releaseLimitConnection()){ log.info("釋放第{}個數據庫鏈接成功",i+1); }else { log.info("釋放第{}個數據庫鏈接失敗",i+1); } } log.info("模仿結束"); }
```
以上代碼邏輯如下:
1. 獲得 Mysql 鏈接邏輯:for 循環獲取鏈接,1~10 都可以獲得鏈接,11~12 獲取不到鏈接,因為鏈接被用完了;
2. 釋放鎖邏輯:for 循環釋放鏈接,1~10 都可以釋放成功,11~12 釋放失敗。
我們看下運行結果,如下圖:

從運行的結果,可以看出,我們實現的 ShareLock 鎖已經完成了 Mysql 鏈接共享的場景了。
### 4 總結
同學們閱讀到這里不知道有沒有兩點感受:
1. 重寫鎖真的很簡單,最關鍵的是要和場景完美貼合,能滿足業務場景的鎖才是好鎖;
2. 鎖其實只是來滿足業務場景的,本質都是 AQS,所以只要 AQS 學會了,在了解清楚場景的情況下,重寫鎖都不難的。
鎖章節最核心的就是 AQS 源碼解析的兩章,只要我們把 AQS 弄懂了,其余鎖的實現,只要稍微看下源碼實現,幾乎馬上就能知道其底層實現的原理,大多數都是通過操作 state 來完成不同的場景需求,所以還是建議大家多看 AQS 源碼,多 debug 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 源碼和面試真題