溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

Java重寫鎖的設(shè)計(jì)結(jié)構(gòu)和細(xì)節(jié)是什么

發(fā)布時(shí)間:2022-03-11 16:25:16 來源:億速云 閱讀:124 作者:iii 欄目:開發(fā)技術(shù)

這篇文章主要介紹“Java重寫鎖的設(shè)計(jì)結(jié)構(gòu)和細(xì)節(jié)是什么”的相關(guān)知識(shí),小編通過實(shí)際案例向大家展示操作過程,操作方法簡(jiǎn)單快捷,實(shí)用性強(qiáng),希望這篇“Java重寫鎖的設(shè)計(jì)結(jié)構(gòu)和細(xì)節(jié)是什么”文章能幫助大家解決問題。

    引導(dǎo)語

    有的面試官喜歡讓同學(xué)在說完鎖的原理之后,讓你重寫一個(gè)新的鎖,要求現(xiàn)場(chǎng)在白板上寫出大概的思路和代碼邏輯,這種面試題目,蠻難的,我個(gè)人覺得其側(cè)重點(diǎn)主要是兩個(gè)部分:

    考察一下你對(duì)鎖原理的理解是如何來的,如果你對(duì)源碼沒有解讀過的話,只是看看網(wǎng)上的文章,或者背面試題,也是能夠說出大概的原理,但你很難現(xiàn)場(chǎng)寫出一個(gè)鎖的實(shí)現(xiàn)代碼,除非你真的看過源碼,或者有和鎖相關(guān)的項(xiàng)目經(jīng)驗(yàn);

    我們不需要?jiǎng)?chuàng)造,我們只需要模仿 Java 鎖中現(xiàn)有的 API 進(jìn)行重寫即可。

    如果你看過源碼,這道題真的很簡(jiǎn)單,你可以挑選一個(gè)你熟悉的鎖進(jìn)行模仿。

    1、需求

    一般自定義鎖的時(shí)候,我們都是根據(jù)需求來進(jìn)行定義的,不可能憑空定義出鎖來,說到共享鎖,大家可能會(huì)想到很多場(chǎng)景,比如說對(duì)于共享資源的讀鎖可以是共享的,比如對(duì)于數(shù)據(jù)庫鏈接的共享訪問,比如對(duì)于 Socket 服務(wù)端的鏈接數(shù)是可以共享的,場(chǎng)景有很多,我們選擇共享訪問數(shù)據(jù)庫鏈接這個(gè)場(chǎng)景來定義一個(gè)鎖。

    2、詳細(xì)設(shè)計(jì)

    假定(以下設(shè)想都為假定)我們的數(shù)據(jù)庫是單機(jī) mysql,只能承受 10 個(gè)鏈接,創(chuàng)建數(shù)據(jù)庫鏈接時(shí),我們是通過最原始 JDBC 的方式,我們用一個(gè)接口把用 JDBC 創(chuàng)建鏈接的過程進(jìn)行了封裝,這個(gè)接口我們命名為:創(chuàng)建鏈接接口。

    共享訪問數(shù)據(jù)庫鏈接的整體要求如下:所有請(qǐng)求加在一起的 mysql 鏈接數(shù),最大不能超過 10(包含 10),一旦超過 10,直接報(bào)錯(cuò)。

    在這個(gè)背景下,我們進(jìn)行了下圖的設(shè)計(jì):

    Java重寫鎖的設(shè)計(jì)結(jié)構(gòu)和細(xì)節(jié)是什么

    這個(gè)設(shè)計(jì)最最關(guān)鍵的地方,就是我們通過能否獲得鎖,來決定是否可以得到 mysql 鏈接,如果能獲得鎖,那么就能得到鏈接,否則直接報(bào)錯(cuò)。

    接著我們一起來看下落地的代碼:

    2.1、定義鎖 

    首先我們需要定義一個(gè)鎖出來,定義時(shí)需要有兩個(gè)元素:

    鎖的定義:同步器 Sync;鎖對(duì)外提供的加鎖和解鎖的方法。

    共享鎖的代碼實(shí)現(xiàn)如下:

    // 共享不公平鎖
    public class ShareLock implements Serializable{
    	// 同步器
      private final Sync sync;
      // 用于確保不能超過最大值
      private final int maxCount;
      /**
       * 初始化時(shí)給同步器 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);
      }
    }

    從上述代碼中可以看出,加鎖和釋放鎖的實(shí)現(xiàn),都依靠同步器 Sync 的底層實(shí)現(xiàn)。

    唯一需要注意的是,鎖需要規(guī)定好 API 的規(guī)范,主要是兩方面:

    API 需要什么,就是鎖在初始化的時(shí)候,你需要傳哪些參數(shù)給我,在 ShareLock 初始化時(shí),需要傳最大可共享鎖的數(shù)目;

    需要定義自身的能力,即定義每個(gè)方法的入?yún)⒑统鰠ⅰT?ShareLock 的實(shí)現(xiàn)中,加鎖和釋放鎖的入?yún)⒍紱]有,是方法里面寫死的 1,表示每次方法執(zhí)行,只能加鎖一次或釋放鎖一次,出參是布爾值,true 表示加鎖或釋放鎖成功,false 表示失敗,底層使用的都是 Sync 非公平鎖。

    以上這種思考方式是有方法論的,就是我們?cè)谒伎家粋€(gè)問題時(shí),可以從兩個(gè)方面出發(fā):API 是什么?API 有什么能力?

    2.2、定義同步器 Sync

    Sync 直接繼承 AQS ,代碼如下:

    class Sync extends AbstractQueuedSynchronizer {
       // 表示最多有 count 個(gè)共享鎖可以獲得
      public Sync(int count) {
        setState(count);
      }
      // 獲得 i 個(gè)鎖
      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 成功獲得鎖,失敗繼續(xù) for 循環(huán)
          if(compareAndSetState(state,expectState)){
            return true;
          }
        }
      }
      // 釋放 i 個(gè)鎖
      @Override
      protected boolean tryReleaseShared(int arg) {
        for(;;){
          if(arg<=0){
            return false;
          }
          int state = getState();
          int expectState = state + arg;
          // 超過了 int 的最大值,或者 expectState 超過了我們的最大預(yù)期
          if(expectState < 0 || expectState > maxCount){
            log.error("state 超過預(yù)期,當(dāng)前 state is {},計(jì)算出的 state is {}",state
            ,expectState);
            return false;
          }
          if(compareAndSetState(state, expectState)){
            return true;
          }
        }
      }
    }

    整個(gè)代碼比較清晰,我們需要注意的是:

    邊界的判斷,比如入?yún)⑹欠穹欠ǎ尫沛i時(shí),會(huì)不會(huì)出現(xiàn)預(yù)期的 state 非法等邊界問題,對(duì)于此類問題我們都需要加以判斷,體現(xiàn)出思維的嚴(yán)謹(jǐn)性;

    加鎖和釋放鎖,需要用 for 自旋 + CAS 的形式,來保證當(dāng)并發(fā)加鎖或釋放鎖時(shí),可以重試成功。寫 for 自旋時(shí),我們需要注意在適當(dāng)?shù)臅r(shí)機(jī)要 return,不要造成死循環(huán),CAS 的方法 AQS 已經(jīng)提供了,不要自己寫,我們自己寫的 CAS 方法是無法保證原子性的。

    2.3、通過能否獲得鎖來決定能否得到鏈接

    鎖定義好了,我們需要把鎖和獲取 Mysql 鏈接結(jié)合起來,我們寫了一個(gè) Mysql 鏈接的工具類,叫做 MysqlConnection,其主要負(fù)責(zé)兩大功能:

    通過 JDBC 建立和 Mysql 的鏈接;

    結(jié)合鎖,來防止請(qǐng)求過大時(shí),Mysql 的總鏈接數(shù)不能超過 10 個(gè)。

    首先我們看下 MysqlConnection 初始化的代碼:

    public class MysqlConnection {
      private final ShareLock lock;
      // maxConnectionSize 表示最大鏈接數(shù)
      public MysqlConnection(int maxConnectionSize) {
        lock = new ShareLock(maxConnectionSize);
      }
    }

    我們可以看到,在初始化時(shí),需要制定最大的鏈接數(shù)是多少,然后把這個(gè)數(shù)值傳遞給鎖,因?yàn)樽畲蟮逆溄訑?shù)就是 ShareLock 鎖的 state 值。

    接著為了完成 1,我們寫了一個(gè) private 的方法:

    // 得到一個(gè) mysql 鏈接,底層實(shí)現(xiàn)省略
    private Connection getConnection(){}

    然后我們實(shí)現(xiàn) 2,代碼如下:

    // 對(duì)外獲取 mysql 鏈接的接口
    // 這里不用try finally 的結(jié)構(gòu),獲得鎖實(shí)現(xiàn)底層不會(huì)有異常
    // 即使出現(xiàn)未知異常,也無需釋放鎖
    public Connection getLimitConnection() {
      if (lock.lock()) {
        return getConnection();
      }
      return null;
    }
    // 對(duì)外釋放 mysql 鏈接的接口
    public boolean releaseLimitConnection() {
      return lock.unLock();
    }

    邏輯也比較簡(jiǎn)單,加鎖時(shí),如果獲得了鎖,就能返回 Mysql 的鏈接,釋放鎖時(shí),在鏈接關(guān)閉成功之后,調(diào)用 releaseLimitConnection 方法即可,此方法會(huì)把鎖的 state 狀態(tài)加一,表示鏈接被釋放了。

    以上步驟,針對(duì) Mysql 鏈接限制的場(chǎng)景鎖就完成了。

    3、測(cè)試

    鎖寫好了,接著我們來測(cè)試一下,我們寫了一個(gè)測(cè)試的 demo,代碼如下:

    public static void main(String[] args) {
      log.info("模仿開始獲得 mysql 鏈接");
      MysqlConnection mysqlConnection = new MysqlConnection(10);
      log.info("初始化 Mysql 鏈接最大只能獲取 10 個(gè)");
      for(int i =0 ;i<12;i++){
        if(null != mysqlConnection.getLimitConnection()){
          log.info("獲得第{}個(gè)數(shù)據(jù)庫鏈接成功",i+1);
        }else {
          log.info("獲得第{}個(gè)數(shù)據(jù)庫鏈接失?。簲?shù)據(jù)庫連接池已滿",i+1);
        }
      }
      log.info("模仿開始釋放 mysql 鏈接");
      for(int i =0 ;i<12;i++){
        if(mysqlConnection.releaseLimitConnection()){
          log.info("釋放第{}個(gè)數(shù)據(jù)庫鏈接成功",i+1);
        }else {
          log.info("釋放第{}個(gè)數(shù)據(jù)庫鏈接失敗",i+1);
        }
      }
      log.info("模仿結(jié)束");
    }

    以上代碼邏輯如下:

    獲得 Mysql 鏈接邏輯:for 循環(huán)獲取鏈接,1~10 都可以獲得鏈接,11~12 獲取不到鏈接,因?yàn)殒溄颖挥猛炅?;釋放鎖邏輯:for 循環(huán)釋放鏈接,1~10 都可以釋放成功,11~12 釋放失敗。

    我們看下運(yùn)行結(jié)果,如下圖:

    Java重寫鎖的設(shè)計(jì)結(jié)構(gòu)和細(xì)節(jié)是什么

    從運(yùn)行的結(jié)果,可以看出,我們實(shí)現(xiàn)的 ShareLock 鎖已經(jīng)完成了 Mysql 鏈接共享的場(chǎng)景了。

    關(guān)于“Java重寫鎖的設(shè)計(jì)結(jié)構(gòu)和細(xì)節(jié)是什么”的內(nèi)容就介紹到這里了,感謝大家的閱讀。如果想了解更多行業(yè)相關(guān)的知識(shí),可以關(guān)注億速云行業(yè)資訊頻道,小編每天都會(huì)為大家更新不同的知識(shí)點(diǎn)。

    向AI問一下細(xì)節(jié)

    免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

    AI