java架構之路(多線程)AQS之ReetrantLock顯示鎖的使用和底層源碼解讀

  說完了咱們的synchronized,此次咱們來講說咱們的顯示鎖ReetrantLock。java

上期回顧:node

  上次博客咱們主要說了鎖的分類,synchronized的使用,和synchronized隱式鎖的膨脹升級過程,從無鎖是如何一步步升級到咱們的重量級鎖的,還有咱們的逃逸分析。編程

鎖的粗化和鎖的消除安全

  這個原本應該是在synchronized裏面去說的,忘記了,不是很重要,可是須要知道有這麼一個東西啦。多線程

  咱們先來演示一下鎖的粗化:併發

StringBuffer sb = new StringBuffer();

public void lockCoarseningMethod(){
    //jvm的優化,鎖的粗化
    sb.append("1");

    sb.append("2");

    sb.append("3");

    sb.append("4");
}

  咱們都知道咱們的StringBuffer是線程安全的,也就是說咱們的StringBuffer是用synchronized修飾過的。那麼咱們能夠得出咱們的4次append都應該是套在一個synchronized裏面的。app

StringBuffer sb = new StringBuffer();

public void lockCoarseningMethod() {
    synchronized (Test.class) {
        sb.append("1");
    }

    synchronized (Test.class) {
        sb.append("2");
    }
    synchronized (Test.class) {
        sb.append("3");
    }
    synchronized (Test.class) {
        sb.append("4");
    }
}

  按照理論來講應該是這樣的,其實JVM對synchronized作了優化處理,底層會優化成一次的synchronized修飾,感興趣的能夠用javap -c 本身看一下,這裏就不帶你們去看了,我之前的博客有javap看彙編指令碼的過程。框架

StringBuffer sb = new StringBuffer();

public void lockCoarseningMethod() {
    synchronized (Test.class) {
        sb.append("1");
        sb.append("2");
        sb.append("3");
        sb.append("4");
    }
}

  再來看一下鎖的消除,其實這個鎖的消除,真的對於synchronized理解了,鎖的消除一眼就知道是什麼了。less

public static void main(String[] args) {
    synchronized (new Object()){
        System.out.println("開始處理邏輯");
    }
}

  對於synchronized而言,咱們每次去鎖的都是對象,而你每次都建立的一個新對象,那還鎖毛線了,每一個線程均可以拿到對象,均可以拿到對象鎖啊,因此沒不會產生鎖的效果了。jvm

概述AQS:

  AQS是AbstractQueuedSynchronizer的簡稱,字面意思,抽象隊列同步器。Java併發編程核心在於java.concurrent.util包而juc當中的大多數同步器 實現都是圍繞着共同的基礎行爲,好比等待隊列、條件隊列、獨佔獲取、共享獲 取等,而這個行爲的抽象就是基於AbstractQueuedSynchronizer簡稱AQS,AQS定 義了一套多線程訪問共享資源的同步器框架,是一個依賴狀態(state)的同步器。就是咱們上次博客說的什麼公平鎖,獨佔鎖等等。

AQS具有特性
  • 阻塞等待隊列
  • 共享/獨佔
  • 公平/非公平
  • 可重入
  • 容許中斷

AQS的簡單原理解讀:

  ReetrantLock的內部功能仍是很強大的,有不少的功能,咱們來一點點縷縷。如Lock,Latch,Barrier等,都是基於AQS框架實現,通常經過定義內部類Sync繼承AQS將同步器全部調用都映射到Sync對應的方法AQS內部維護屬性volatile int state (32位),state表示資源的可用狀態

  • State三種訪問方式
  1. getState()
  2. setState()
  3. compareAndSetState()
  • AQS定義兩種資源共享方式
  1. Exclusive-獨佔,只有一個線程能執行,如ReentrantLock
  2. Share-共享,多個線程能夠同時執行,如Semaphore/CountDownLatch
  • AQS定義兩種隊列
  1. 同步等待隊列
  2. 條件等待隊列
  • AQS已經在頂層實現好了。自定義同步器實現時主要實現如下幾種方法:
  1. isHeldExclusively():該線程是否正在獨佔資源。只有用到condition才須要去實現它。
  2. tryAcquire(int):獨佔方式。嘗試獲取資源,成功則返回true,失敗則返回false。
  3. tryRelease(int):獨佔方式。嘗試釋放資源,成功則返回true,失敗則返回false。
  4. tryAcquireShared(int):共享方式。嘗試獲取資源。負數表示失敗;0表示成功,但沒有剩餘可用資源;正數表示成功,且有剩餘資源。
  5. tryReleaseShared(int):共享方式。嘗試釋放資源,若是釋放後容許喚醒後續等待結點返回true,不然返回false。

  剛纔提到那麼多屬性,可能會有一些懵,咱們來看一下ReentrantLock內部是怎麼來實現哪些鎖的吧。

  打開咱們的ReetrantLock源代碼能夠看到一個關鍵的屬性

private final Sync sync;

  後面有一個抽象方法而且繼承了AbstractQueuedSynchronizer類,內部有一個用volatile修飾過的整型變量state,他就是用來記錄上鎖次數的,這樣就實現了咱們剛纔的說的重入鎖和非可重入鎖。咱們來畫一個圖。

   AbstractQueuedSynchronizer這個類裏面定義了詳細的ReetrantLock的屬性,後面我會一點點去說,帶着解讀一下源碼(上面都是摘自源碼的)。state和線程exclusiveOwnerThread比較好理解,最後那個隊列可能不太好弄,我這裏寫的也是比較泛化的,後面我會弄一個專題一個個去說。 相面說的CLH隊列其實不是很準確,咱們能夠理解爲就是一個泛型爲Node的雙向鏈表結構就能夠了。

  等待隊列中Node節點內還有三個很重要的屬性就是prev前驅指針指向咱們的前一個Node節點,和一個next後繼指針來指向咱們的下一個Node節點,這樣就造成了一個雙向鏈表的結構,於此同時還有一個Thread來記錄咱們的當前線程。

  在條件隊列中,prev和next指針都是null的,不論是什麼隊列,他都有一個waitStatus的屬性來記錄咱們的節點狀態的,就是咱們剛纔說的CANCELLED結束、SIGNAL可喚醒那四個常量值。

AQS中ReetrantLock的使用:

  公平鎖和非公平鎖:這個仍是比較好記憶的,舉一個栗子,咱們去車站排隊上車,總有**插隊,用蛇形走位能夠上車的是吧,這就是一個非公平的鎖,若是說,咱們在排隊的時候加上護欄,每次只能排一我的,他人沒法插隊的,這時就是一個公平鎖。總之就是不加塞的就是公平的,咱們都討厭不公平。

   重入鎖與非可重入鎖:這個也很好理解,重入鎖就是當咱們的線程A拿到鎖之後,能夠繼續去拿多把鎖,而後再陸陸續續的作完任務再去解鎖,非可重入呢,就是隻能得到一把鎖,若是想獲取多把鎖,很差意思,去後面排下隊伍。下面我化了一個重入鎖的栗子,快過年了,你們提着行李回老家,咱們進去了會一併帶着行李進去(不帶行李的基本是行李丟了),這就是一個重入鎖的栗子,咱們人進去了得到通道經過(鎖),而後咱們也拖着行李得到了通道經過(鎖),而後咱們才空出通道供後面的人使用。若是是非可重入鎖就是人進去就進去吧,行李再次排隊,說不許何時能進來。

   上一段代碼來驗證一下咱們上面說的那些知識點。

import java.util.concurrent.locks.ReentrantLock;

public class Test {

    private ReentrantLock lock = new ReentrantLock(true);//true公平鎖,false非公平鎖


    public void lockMethod(String threadName) {
        lock.lock();
        System.out.println(threadName + "獲得了一把鎖1");

        lock.lock();
        System.out.println(threadName + "獲得了一把鎖2");

        lock.lock();
        System.out.println(threadName + "獲得了一把鎖3");

        lock.unlock();
        System.out.println(threadName + "釋放了一把鎖1");

        lock.unlock();
        System.out.println(threadName + "釋放了一把鎖2");

        lock.unlock();
        System.out.println(threadName + "釋放了一把鎖3");
    }


    public static void main(String[] args) {
        Test test = new Test();

        new Thread(() -> {
            String threadName = Thread.currentThread().getName();
            test.lockMethod(threadName);

        }, "線程A").start();
    }

}

   經過代碼閱讀咱們知道咱們弄一個重入鎖,加三次鎖,解三次鎖,咱們來看一下內部sync的變化,調試一下。

    咱們看到了咱們的state變量是用來存儲咱們的入鎖次數的。剛纔去看過源碼的小夥伴知道了咱們的state是經過volatile修飾過的,雖然能夠保證咱們的有序性和可見性,可是一個int++的操做,他是沒法保證原子性的,咱們繼續來深挖一下代碼看看內部是怎麼實現高併發場景下保證數據準確的。點擊lock方法進去,咱們看到lock方法是基於sync來操做的,就是咱們上面的畫的那個ReetrantLock的圖。

/**
 * Sync object for fair locks
 */
static final class FairSync extends Sync {
    private static final long serialVersionUID = -3000897897090466540L;

    final void lock() {//開始加鎖
        acquire(1);
    }

    /**
     * Fair version of tryAcquire.  Don't grant access unless
     * recursive call or no waiters or is first.
     */
    protected final boolean tryAcquire(int acquires) {
        final Thread current = Thread.currentThread();//獲得當前線程
        int c = getState();//獲得上鎖次數
        if (c == 0) {//判斷是否上過鎖
            if (!hasQueuedPredecessors() &&//hasQueuedPredecessors判斷是否有正在等待的節點,
                    compareAndSetState(0, acquires)) {//經過unsafe去更新上鎖次數
                setExclusiveOwnerThread(current);//設置線程
                return true;
            }
        }
        else if (current == getExclusiveOwnerThread()) {
            int nextc = c + acquires;
            if (nextc < 0)
                throw new Error("Maximum lock count exceeded");
            setState(nextc);
            return true;
        }
        return false;
    }
}

  此次咱們開啓多個線程來同時訪問來看一下咱們的Node的變化。同時開啓ABCD四個線程來執行這個

   此次咱們看到了head屬性和tail屬性再也不是空的。head是也是一個node節點,前驅指針是空的,後驅指針指向後繼節點,Thread爲空,tail的node節點正好是和head相對應的節點。這樣的設計就是爲了更好的去驗證隊列中仍是否存在剩餘的線程節點須要處理。而後該線程運行結束之後會喚醒在隊列中的節點,然其它線程繼續運行。

  咱們知道咱們建立的公平鎖,若是說BCD好好的在排隊,E線程來了,只能好好的去排隊,由於公平,因此排隊,若是咱們建立的是非公平鎖,E線程就有機會拿到鎖,拿到就運行,拿不到就去排隊。

相關文章
相關標籤/搜索