有的面試官喜歡讓同學在說完鎖的原理之後,讓你重寫一個新的鎖,要求現場在白板上寫出大概的思路和程式碼邏輯,這種面試題目,蠻難的,我個人覺得其重點主要是兩個部分:
考察一下你對鎖原理的理解是如何來的,如果你對源碼沒有解讀過的話,只是看看網上的文章,或者背面試題,也是能夠說出大概的原理,但你很難現場寫出一個鎖的實現代碼,除非你真的看過源碼,或有和鎖相關的專案經驗;
我們不需要創造,我們只需要模仿Java 鎖定中現有的API 進行重寫即可。
如果你看過原始碼,這題真的很簡單,你可以挑選一個你熟悉的鎖來模仿。
一般自訂鎖的時候,我們都是根據需求來進行定義的,不可能憑空定義出鎖來,說到共享鎖,大家可能會想到很多場景,比如說對於共享資源的讀鎖可以是共享的,例如對於資料庫連結的共享訪問,例如對於Socket 服務端的連結數是可以共享的,場景有很多,我們選擇共享訪問資料庫連結這個場景來定義一個鎖。
假定(以下設想都為假定)我們的資料庫是單機mysql,只能承受10 個鏈接,在建立資料庫連結時,我們是透過最原始JDBC 的方式,我們用一個介面把用JDBC 建立連結的過程進行了封裝,這個介面我們命名為:建立連結介面。
共享存取資料庫連結的整體要求如下:所有請求加在一起的 mysql 連結數,最大不能超過 10(包含 10),一旦超過 10,直接報錯。
在這個背景下,我們進行了下圖的設計:
這個設計最關鍵的地方,就是我們透過能否獲得鎖,來決定是否可以獲得mysql 鏈接,如果能獲得鎖,那麼就能得到鏈接,否則直接報錯。
接著我們一起來看下落地的程式碼:
首先我們需要定義一個鎖出來,定義時需要有兩個元素:
鎖的定義:同步器Sync;鎖對外提供的加鎖和解鎖的方法。
共享鎖定的程式碼實作如下:
// 共享不公平锁 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 的規範,主要是兩方面:
API 需要什麼,就是鎖在初始化的時候,你需要傳哪些參數給我,在ShareLock 初始化時,需要傳最大可共享鎖定的數量;
#需要定義自身的能力,也就是定義每個方法的入參和出參。在ShareLock 的實作中,加鎖和釋放鎖的入參都沒有,是方法裡面寫死的1,表示每次方法執行,只能加鎖一次或釋放鎖一次,出參是布林值,true 表示加鎖或釋放鎖成功,false 表示失敗,底層使用的都是Sync 非公平鎖。
以上這種思考方式是有方法論的,就是我們在思考一個問題時,可以從兩個面向出發:API 是什麼? API 有什麼能力?
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; } } } }
整個程式碼比較清晰,我們要注意的是:
邊界的判斷,例如入參是否非法,釋放鎖時,會不會出現預期的state 非法等邊界問題,對於此類問題我們都需要加以判斷,體現出思維的嚴謹性;
加鎖和釋放鎖,需要用for 自旋CAS 的形式,來確保當並發加鎖或釋放鎖時,可以重試成功。寫 for 自旋時,我們需要注意在適當的時機要 return,不要造成死循環,CAS 的方法 AQS 已經提供了,不要自己寫,我們自己寫的 CAS 方法是無法保證原子性的。
鎖定定義好了,我們需要把鎖和取得Mysql 連結結合起來,我們寫了一個Mysql 連結的工具類,稱為MysqlConnection,其主要負責兩大功能:
透過JDBC 建立和Mysql 的連結;
結合鎖,來防止請求過大時,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 链接限制的场景锁就完成了。
锁写好了,接着我们来测试一下,我们写了一个测试的 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("模仿结束"); }
以上代码逻辑如下:
获得 Mysql 链接逻辑:for 循环获取链接,1~10 都可以获得链接,11~12 获取不到链接,因为链接被用完了;释放锁逻辑:for 循环释放链接,1~10 都可以释放成功,11~12 释放失败。
我们看下运行结果,如下图:
从运行的结果,可以看出,我们实现的 ShareLock 锁已经完成了 Mysql 链接共享的场景了。
以上是Java重寫鎖的設計結構和細節是什麼的詳細內容。更多資訊請關注PHP中文網其他相關文章!