Java併發程式設計序列之JUC底層AQS(二)

我又不是架構師發表於2017-12-22

Java併發程式設計序列之JUC底層AQS(二)

Hello,大家好,在上一篇文章中,作者簡單的把Lock介面和AQS的API,以及關係大致說了一下,本文還是圍繞AQS為話題(AQS是重中之重,這個搞明白了。後面JUC自己看原始碼都很Easy可以看懂),先具體到API大致說下API的對應關係,然後作者自己寫倆自定義Lock說明問題,最後再詳細講解AQS提供的一些模板API具體怎麼實現的.文章結構:

  1. AQS模板API的對應關係。
  2. 自定義倆Lock.
  3. AQS模板API實現.

1. AQS模板API的對應關係。

在上文的講解Lock和AQS關係時,有一張圖大致講解了這些API的呼叫關係,本文作者決定再畫一張,具體到AQS中具體API的對應關係:

Java併發程式設計序列之JUC底層AQS(二)
圖中可以看到綠色箭頭表示獨佔性鎖的實現邏輯,紅色箭頭表示共享式鎖實現的邏輯,所謂的獨佔性鎖意思就是,只要有一個執行緒拿到鎖,其他執行緒全部T出去到佇列等待。共享性鎖就好理解了,一部分個性化(根據tryAcquire返回值決定)的執行緒可以拿到鎖,沒拿到的到佇列。

2. 自定義倆Lock.

好了,根據上面的知識,結合上一節講解的各個模組API呼叫關係,作者不廢話了,來,自定義一個獨佔性鎖:任何情況下,只允許一個執行緒拿到鎖!即使是自己也只能拿到一次!(不可重入!)

/**
 * Created by zdy on 17/12/22.
 */
public class OnlyOneLock implements Lock {
    private static class OnlyOneLockAQS extends AbstractQueuedSynchronizer{
        //在該類內部呼叫State相關API,維護是否獲取到鎖的邏輯
        @Override
        protected boolean tryAcquire(int arg) {
            //狀態為0時,設定為1,表示當前執行緒獲取到鎖
            if(compareAndSetState(0,1)){
                //設定當前執行緒為獲取到鎖的執行緒
                setExclusiveOwnerThread(Thread.currentThread());
                return true ;
            }
            return false;
        }
        @Override
        protected boolean tryRelease(int arg) {
            //如果狀態為0,表示還沒有執行緒獲取到鎖,你釋放什麼釋放
            if(getState()==0) {throw new IllegalMonitorStateException();}
            setExclusiveOwnerThread(null);
            setState(0);
            return true;
        }
        @Override
        protected boolean isHeldExclusively() {
            //判斷該執行緒是否被佔有
            return getState()==1;
        }
    }

    //講所有鎖的語音,直接呼叫aqs的api來實現
    private final OnlyOneLockAQS  aqs=new OnlyOneLockAQS();

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

    @Override
    public void lockInterruptibly() throws InterruptedException {
        aqs.acquireInterruptibly(1);
    }

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

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

    @Override
    public void unlock() {
        //記住一定不能直接呼叫tyrRelease那一套API,因為release方法幫我們維護釋放後的通知邏輯.
        aqs.release(1);
    }

    @Override
    public Condition newCondition() {
        return null;
    }
}
複製程式碼

好了,大家好好理解理解,其實也沒那麼難。可以看到。Lock中的實現API都是呼叫了同步器AQS的模板方法我們實現的方法來實現鎖的邏輯的。我們實現的try開頭的那幾個API根本不用管什麼佇列,什麼通知邏輯。只需要管是否獲取到鎖的邏輯。是不是很神奇?這裡強調一下,其實在Lock介面中只有tryLock()這個API會直接呼叫tryAcquire()這個我們實現的API之外,其他的API其實都是呼叫的AQS的模板方法,應為模板方法封裝了很多複雜的佇列通知等邏輯。

OK,廢話少說,上面可以說是自己寫了一個獨佔性的鎖,永遠只有一個執行緒可以獲取到鎖。下面再定義一個共享性的鎖。來個簡單點的。大家應該都知道限流,現在假如有個需求,一個應用級的限流(業務介面層面的),要求一個介面最後只能被5個執行緒併發訪問,後續的執行緒再訪問,直接返回,不做業務邏輯處理.(常用語秒殺業務中某個商品只有5個,那麼有必要放很多請求進來嗎?)

package com.zdy;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.AbstractQueuedSynchronizer;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;

/**
 * Created by zdy on 17/12/22.
 */
public class ShareLock implements Lock {

    private class ShareLockAQS extends AbstractQueuedSynchronizer{
        protected ShareLockAQS(Integer count) {
            super();
            setState(count);
        }
        @Override
        protected int tryAcquireShared(int arg) {
            for (; ; ) {
                Integer state = getState();
                Integer newCount = state - arg;
                if (newCount < 0 || compareAndSetState(state, newCount)) {
                    return newCount;
                }
            }
        }
        @Override
        protected boolean tryReleaseShared(int arg) {
            for (; ; ) {
                //注意這裡不能直接setState了,因為可能多個執行緒同時release
                Integer state = getState();
                Integer newCount = state + arg;
                if (compareAndSetState(state,newCount)) {
                    return true;
                }
            }
        }
        @Override
        protected boolean isHeldExclusively() {
            return getState()==0;
        }
    }

    private ShareLockAQS aqs=new ShareLockAQS(5);

    @Override
    public void lock() {
        aqs.acquireShared(1);
    }

    @Override
    public void lockInterruptibly() throws InterruptedException {
        aqs.acquireInterruptibly(1);
    }

    @Override
    public boolean tryLock() {
        return aqs.tryAcquireShared(1)>=0;
    }

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

    @Override
    public void unlock() {
        aqs.releaseShared(1);
    }

    @Override
    public Condition newCondition() {
        return null;
    }
}
複製程式碼

大致邏輯:內部維護了一個state,初始化為5,獲取一個鎖,減1,釋放一個鎖,+1。注意獲取和釋放的併發性!尤其是在釋放時,釋放失敗了,一定要for(;;),切記! 一句話說,就是釋放必須是成功的.

下面來驗證一下這個併發Lock:

Java併發程式設計序列之JUC底層AQS(二)
其實思路還是比較清晰的。在web請求介面中每進來就去取鎖。由於我們的鎖最多隻能取5次,所有當第6次請求進來後直接return "活動已經結束...",結果我就不演示了,這就是我們的限流Lock了。看,限流沒想的那麼高大上。當然了,我這裡說的只是應用限流的一種,其實粗暴一點,直接用個Integer變數,加鎖每次減一就可以了。後面JUC裡面還有許多內建的工具類來提供給我們使用。說到限流這一塊,其實應用層面介面的限流是最Low的,效果最不好旳,為什麼呢?因為介面已經進入到容器了,對資源的消耗也是存在的。其實最標準的限流都是在閘道器或者接入層,比如Nginx層面的限流,效果比較顯著。好了好了,又扯這麼多。回到主題...

3. AQS模板API實現.

通過上面的例子可以知道,其實我們想要實現自己的同步元件還是比較簡單的。只需要寫好AQS,而AQS中需要我們覆蓋的幾個方法只需要處理好拿到鎖和沒有拿到鎖的邏輯,至於執行緒怎麼維護,我們還是不清楚的,這一小節就帶大家揭開這個神祕的面紗。先說簡單獨佔Lock的,比如在我們重寫了tryAcquire時,AQS的模板方法acquire內部會呼叫這個方法,然後維護佇列邏輯。廢話不多說直接上原始碼:(程式碼全為原始碼,註釋為小弟所加)

    public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }
複製程式碼

先呼叫tryAcquire();如果拿到,程式碼直接跑完,不阻塞。 如果沒有拿到。呼叫addWaiter();把當前執行緒構建成一個Node加入到佇列尾部。

    private Node addWaiter(Node mode) {
        Node node = new Node(Thread.currentThread(), mode);
        Node pred = tail;
        if (pred != null) {
            node.prev = pred;
            //Cas演算法設定進去。
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
        enq(node);
        return node;
    }
複製程式碼

把當前執行緒加入到佇列(FIFO)尾部後,呼叫acquireQueued();

    final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                final Node p = node.predecessor();
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                //parkAndCheckInterrupt這個API會Park當前執行緒
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }
複製程式碼

這個API賊有意思,先檢查當前執行緒在佇列中的前一個執行緒是否為頭結點,如果是就在嘗試拿一次,沒拿到的話會在parkAndCheckInterrupt時把自己給Park掉,Park大致可以理解為休眠(Sleep),這個時候,執行緒就老老實實的在佇列裡面等著,等什麼呢?等獲取鎖的執行緒釋放鎖後通知它,它好繼續在for迴圈裡面獲取鎖。。 我貼一下流程圖。然後大家根據原始碼對著看一看。因為原始碼細節比較多。不可能一一講解。

Java併發程式設計序列之JUC底層AQS(二)

大家要明白,這個流程是獨佔式的佇列流程。首節點永遠為獲取到鎖的那個節點。

然後說下共享式的佇列如何維護,還拿上面那個例子,支援併發5個執行緒獲取到鎖:

  1. 入佇列:執行緒獲取鎖失敗後,建立一個節點,並將節點新增到等待佇列尾,然後將執行緒阻塞,等待喚醒;
  2. 喚醒:另一個執行緒釋放鎖,取佇列的第一個節點,將節點對應執行緒喚醒;
  3. 出佇列:喚醒後的執行緒將嘗試獲取鎖,成功後將自己移出佇列,同時判斷是否任然存在空閒的鎖,如果存在則繼續喚醒下一個節點。
  4. 每次只會喚醒第一個節點,如果同時釋放多個鎖,後續的節點將由前面被喚醒的節點來喚醒,儘量減少資料競爭。

這一塊的邏輯和獨佔Lock還是很大差別的,我就不和大家Show原始碼了。還是比較麻煩的。大致和獨佔Lock一樣。只是佇列的維護不一樣,大家感興趣自己看一看。 順便提一個小知識點: Sleep()和Object.wait()遇到Interrupt出異常。
LockSupport.park()遇到則會喚醒繼續執行。

結語

好了,其實AQS這一塊原始碼翻起來遠不止這麼多,我這裡只是大致說了個主線,佇列如何入隊,如何喚醒,稍微說了下,感興趣的同學可以再仔細琢磨,因為文字確實不太好跟原始碼。Have a good day .

相關文章