使用Java的AQS組件自定義一把鎖

使用Java的AQS組件自定義一把鎖

AQS

AQS全稱是 AbstractQueuedSynchronizer,也稱“同步器”,是阻塞式鎖和相關的同步器工具的框架

AQS有如下特點:

  • 用 state 屬性來表示資源的狀態(分獨佔模式和共享模式),子類需要定義如何維護這個狀態,控制如何獲取鎖和釋放鎖
    • getState - 獲取 state 狀態
    • setState - 設置 state 狀態
    • compareAndSetState - 通過cas 機制設置 state 狀態
    • 獨佔模式是隻有一個線程能夠訪問資源,而共享模式可以允許多個線程訪問資源
  • 提供了基於 FIFO 的等待隊列,類似於 Monitor 的 EntryList
  • 條件變量來實現等待、喚醒機制,支持多個條件變量,類似於 Monitor 的 WaitSet(ConditionObject)

同步器AQS的子類主要對以下方法進行實現

  • tryAcquire():嘗試獲取鎖

  • tryRelease():嘗試釋放鎖

  • tryAcquireShared()

  • tryReleaseShared()

  • isHeldExclusively():判斷是否線程獨佔

獲取鎖的姿勢

 //如果獲取鎖失敗
if (!tryAcquire(arg)) {
 // 入隊, 可以選擇阻塞當前線程 park unpark
}

釋放鎖的姿勢

 //如果釋放鎖成功
if (tryRelease(arg)) {
 // 讓阻塞線程恢復運行
}

使用AQS自定義一把不可重入鎖

第一步:實現一個同步器(實現AQS)

只需要實現幾個基本功能,則AQS的其他默認實現會調用你實現的基本功能

/*自定義同步器,只需要繼承AQS,並實現基本功能即可*/
final class MySync extends AbstractQueuedSynchronizer {

    /*實現獲取鎖*/
    @Override
    protected boolean tryAcquire(int acquires) {
        if (acquires==1){
            //0代表無線程持有鎖,1代表有線程持有鎖,此處通過CAS將鎖狀態邊爲1
            if (compareAndSetState(0,1)){
                setExclusiveOwnerThread(Thread.currentThread());
                return true ;
            }
        }
        return false ;
    }

    /*實現釋放鎖*/
    @Override
    protected boolean tryRelease(int acquires) {
        if (acquires == 1){
            //如果鎖狀態爲0,即無線程持有,拋出對象頭狀態異常
            if (getState()==0) throw new IllegalMonitorStateException();
            //將同步器的持有線程置空
            setExclusiveOwnerThread(null);
            setState(0);
            return true ;
        }
        return false ;
    }


    /*鎖是否被佔用*/
    @Override
    protected boolean isHeldExclusively() {
        return getState()==1  ;
    }

    /*自己添加一個返回條件變量的方法*/
    protected Condition newCondition(){
        return new ConditionObject();
    }
}

第二步:編寫一個鎖類,需要繼承JUC的Lock接口,該接口定義了一個鎖的基本方法,利用我們上面的AQS,可以很輕鬆的實現Lock接口

class MyLock implements Lock{
    //一個myLock對象共享一個同步器,故用static
    static MySync sync = new MySync() ;

    @Override
    public void lock() {
        sync.acquire(1);
    }

    @Override
    public void lockInterruptibly() throws InterruptedException {
        //使用同步器的默認實現
        sync.acquireInterruptibly(1);
    }

    @Override
    public boolean tryLock() {
        return sync.tryAcquire(1);
    }

    @Override
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
        return sync.tryAcquireNanos(1,unit.toNanos(time));
    }

    @Override
    public void unlock() {
        sync.release(1) ;
    }

    @Override
    public Condition newCondition() {
        //返回AQS的條件變量
        return sync.newCondition();
    }
    
}

測試:

    /*test*/
    public static void main(String[] args) {
        MyLock lock = new MyLock() ;
        new Thread(()->{
            lock.lock();
            try {
                System.out.println("t1 locking ..."+new Date());
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }finally {
                System.out.println("t1 unlocking ..."+new Date());
                lock.unlock();
            }
        }).start();

        new Thread(()->{
            lock.lock();
            try {
                System.out.println("t2 locking ..."+new Date());
            }finally {
                System.out.println("t2 unlocking ..."+new Date());
                lock.unlock();
            }
        }).start();
    }

結果:

t1 locking …Sat Jun 20 19:30:15 CST 2020
t1 unlocking …Sat Jun 20 19:30:16 CST 2020
t2 locking …Sat Jun 20 19:30:16 CST 2020
t2 unlocking …Sat Jun 20 19:30:16 CST 2020

t1先上鎖,等待一秒解鎖,t2得以執行,證明這個自定義鎖寫的沒問題

AQS想法小結

AQS 要實現的功能目標

  • 阻塞版本獲取鎖 acquire 和非阻塞的版本嘗試獲取鎖 tryAcquire

  • 獲取鎖超時機制

  • 通過打斷取消機制

  • 獨佔機制及共享機制

  • 條件不滿足時的等待機制

AQS設計小結

AQS 的基本思想其實很簡單

  • 獲取鎖的邏輯
while(state 狀態不允許獲取) {
 if(隊列中還沒有此線程) {
 入隊並阻塞
 }
}
當前線程從阻塞隊列出隊
  • 釋放鎖的邏輯
if(state 狀態允許了) {
 恢復阻塞的線程(s) }

要點

  • 原子維護 state 狀態
  • 阻塞及恢復線程
  • 維護隊列

state設計

  • state 使用 volatile 配合 cas 保證其修改時的原子性
  • state 使用了 32bit int 來維護同步狀態,因爲當時使用 long 在很多平臺下測試的結果並不理想

阻塞恢復設計

  • 早期的控制線程暫停和恢復的 api 有 suspend 和 resume,但它們是不可用的,因爲如果先調用的 resume 那麼 suspend 將感知不到

  • 解決方法是使用 park & unpark 來實現線程的暫停和恢復,具體原理在之前講過了,先 unpark 再 park 也沒問題

  • park & unpark 是針對線程的,而不是針對同步器的,因此控制粒度更爲精細

  • park 線程還可以通過 interrupt 打斷

Sychronized原理的最後,我特別提到了park&unpark的原理,不清楚的可翻閱

阻塞隊列設計

  • 使用了 FIFO 先入先出隊列,並不支持優先級隊列

  • 設計時借鑑了 CLH 隊列,它是一種單向無鎖隊列

    • CLH(Craig,Landin,and Hagersten)隊列是一個虛擬的雙向隊列(虛擬的雙向隊列即不存在隊列實例,僅存在結點之間的關聯關係)。AQS 是將每條請求共享資源的線程封裝成一個 CLH 鎖隊列的一個結點(Node)來實現鎖的分配。

      隊列中有 head 和 tail 兩個指針節點,都用 volatile 修飾配合 cas 使用,每個節點有 state 維護節點狀態

      入隊僞代碼,只需要考慮 tail 賦值的原子性

      do {
       // 原來的 tail
       Node prev = tail;
       // 用 cas 在原來 tail 的基礎上改爲 node
      } while(tail.compareAndSet(prev, node))
      

      出隊僞代碼

      // prev 是上一個節點,上一個節點如果是喚醒狀態
      while((Node prev=node.prev).state != 喚醒狀態) {
      }
      // 設置頭節點
      head = node;
      

      CLH 好處:

      • 無鎖自旋
      • 快捷無阻塞

      AQS對CLH的改進:

      private Node enq(final Node node) {
       for (;;) {
       Node t = tail;
       // 隊列中還沒有元素 tail 爲 null
       if (t == null) {
       // 將 head 從 null -> dummy
       if (compareAndSetHead(new Node()))
       tail = head;
       } else {
       // 將 node 的 prev 設置爲原來的 tail
       node.prev = t;
       // 將 tail 從原來的 tail 設置爲 node
       if (compareAndSetTail(t, node)) {
       // 原來 tail 的 next 設置爲 node
       t.next = node;
       return t;
       }
       }
       }
      }
      

主要用到 AQS 的併發工具類

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章