首頁 >Java >java教程 >Java重寫鎖的設計結構和細節是什麼

Java重寫鎖的設計結構和細節是什麼

王林
王林轉載
2023-04-18 17:22:031209瀏覽

    引導語

    有的面試官喜歡讓同學在說完鎖的原理之後,讓你重寫一個新的鎖,要求現場在白板上寫出大概的思路和程式碼邏輯,這種面試題目,蠻難的,我個人覺得其重點主要是兩個部分:

    考察一下你對鎖原理的理解是如何來的,如果你對源碼沒有解讀過的話,只是看看網上的文章,或者背面試題,也是能夠說出大概的原理,但你很難現場寫出一個鎖的實現代碼,除非你真的看過源碼,或有和鎖相關的專案經驗;

    我們不需要創造,我們只需要模仿Java 鎖定中現有的API 進行重寫即可。

    如果你看過原始碼,這題真的很簡單,你可以挑選一個你熟悉的鎖來模仿。

    1、需求

    一般自訂鎖的時候,我們都是根據需求來進行定義的,不可能憑空定義出鎖來,說到共享鎖,大家可能會想到很多場景,比如說對於共享資源的讀鎖可以是共享的,例如對於資料庫連結的共享訪問,例如對於Socket 服務端的連結數是可以共享的,場景有很多,我們選擇共享訪問資料庫連結這個場景來定義一個鎖。

    2、詳細設計

    假定(以下設想都為假定)我們的資料庫是單機mysql,只能承受10 個鏈接,在建立資料庫連結時,我們是透過最原始JDBC 的方式,我們用一個介面把用JDBC 建立連結的過程進行了封裝,這個介面我們命名為:建立連結介面。

    共享存取資料庫連結的整體要求如下:所有請求加在一起的 mysql 連結數,最大不能超過 10(包含 10),一旦超過 10,直接報錯。

    在這個背景下,我們進行了下圖的設計:

    Java重寫鎖的設計結構和細節是什麼

    這個設計最關鍵的地方,就是我們透過能否獲得鎖,來決定是否可以獲得mysql 鏈接,如果能獲得鎖,那麼就能得到鏈接,否則直接報錯。

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

    2.1、定義鎖定 

    首先我們需要定義一個鎖出來,定義時需要有兩個元素:

    鎖的定義:同步器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 有什麼能力?

    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;
          }
        }
      }
    }

    整個程式碼比較清晰,我們要注意的是:

    邊界的判斷,例如入參是否非法,釋放鎖時,會不會出現預期的state 非法等邊界問題,對於此類問題我們都需要加以判斷,體現出思維的嚴謹性;

    加鎖和釋放鎖,需要用for 自旋CAS 的形式,來確保當並發加鎖或釋放鎖時,可以重試成功。寫 for 自旋時,我們需要注意在適當的時機要 return,不要造成死循環,CAS 的方法 AQS 已經提供了,不要自己寫,我們自己寫的 CAS 方法是無法保證原子性的。

    2.3、透過能否獲得鎖來決定能否得到連結

    鎖定定義好了,我們需要把鎖和取得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 链接限制的场景锁就完成了。

    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("模仿结束");
    }

    以上代码逻辑如下:

    获得 Mysql 链接逻辑:for 循环获取链接,1~10 都可以获得链接,11~12 获取不到链接,因为链接被用完了;释放锁逻辑:for 循环释放链接,1~10 都可以释放成功,11~12 释放失败。

    我们看下运行结果,如下图:

    Java重寫鎖的設計結構和細節是什麼

    从运行的结果,可以看出,我们实现的 ShareLock 锁已经完成了 Mysql 链接共享的场景了。

    以上是Java重寫鎖的設計結構和細節是什麼的詳細內容。更多資訊請關注PHP中文網其他相關文章!

    陳述:
    本文轉載於:yisu.com。如有侵權,請聯絡admin@php.cn刪除