1. 程式人生 > >Java併發程式設計——AQS

Java併發程式設計——AQS

本文轉至 連結太長 ,如果頁面失效,請直接關注微信公眾號 “小孩子4919 我們都是小青蛙查詢文章—java併發效能(五)之牛逼的AQS(上)。特此申明!

我們之前都是直接線上程中使用各種同步機制,我們可以把相關的同步問題抽象出來單獨定義一些工具,這些工具可以在合適的併發場景下複用,下邊我們看如何自定義我們自己的同步工具

設計java的大叔們為了我們方便的自定義各種同步工具,為我們提供了大殺器AbstractQueuedSynchronizer類,這是一個抽象類,以下我們會簡稱AQS,翻譯成中文就是抽象佇列同步器。這傢伙老有用了,封裝了各種底層的同步細節,我們程式設計師想自定義自己的同步工具的時候,只需要定義這個類的子類並覆蓋它提供的一些方法就好了。我們前邊用到的顯式鎖ReentrantLock

就是藉助了AQS的神力實現的,現在馬上來看看這個類的實現原理以及如何使用它自定義同步工具。

1.同步狀態

AQS中維護了一個名叫state的欄位,是由volatile修飾的,它就是所謂的同步狀態

private volatile int state;

並且提供了幾個訪問這個欄位的方法:

方法名 描述
protected   final   int   getState( ) 獲取state的值
protected   final   void   setState( int newState ) 設定state的值
protected   final   boolean   compareAndSetState( int  expect, int  update ) 使用CAS方式更新state的值

可以看到這幾個方法都是final修飾的,說明子類中無法重寫它們。另外它們都是protected修飾的,說明只能在子類中使用這些方法。

在一些執行緒協調的場景中,一個執行緒在進行某些操作的時候其他的執行緒都不能執行該操作,比如持有鎖時的操作,在同一時刻只能有一個執行緒持有鎖,我們把這種情景稱為獨佔模式;在另一些執行緒協調的場景中,可以同時允許多個執行緒同時進行某種操作,我們把這種情景稱為共享模式

我們可以通過修改state欄位代表的同步狀態來實現多執行緒的獨佔模式或者共享模式

比如在獨佔模式,我們可以把state的初始值設定成0,每當某個執行緒要進行某項獨佔

操作前,都需要判斷state的值是不是0,如果不是0的話意味著別的執行緒已經進入該操作,則本執行緒需要阻塞等待;如果是0的話就把state的值設定成1,自己進入該操作。這個先判斷再設定的過程我們可以通過CAS操作保證原子性,我們把這個過程稱為嘗試獲取同步狀態。如果一個執行緒獲取同步狀態成功了,那麼在另一個執行緒嘗試獲取同步狀態的時候發現state的值已經是1了就一直阻塞等待,直到獲取同步狀態成功的執行緒執行完了需要同步的操作後釋放同步狀態,也就是把state的值設定為0,並通知後續等待的執行緒。

共享模式的道理也差不多,比如說某項操作我們允許10個執行緒同時進行,超過這個數量的執行緒就需要阻塞等待。那麼我們就可以把state的初始值設定為10,一個執行緒嘗試獲取同步狀態的意思就是先判斷state的值是否大於0,如果不大於0的話意味著當前已經有10個執行緒在同時執行該操作,本執行緒需要阻塞等待;如果state的值大於0,那麼可以把state的值減1後進入該操作,每當一個執行緒完成操作的時候需要釋放同步狀態,也就是把state的值加1,並通知後續等待的執行緒。

所以對於我們自定義的同步工具來說,需要自定義獲取同步狀態與釋放同步狀態的方式,而AQS中的幾個方法正是用來做這個事兒的:

方法名 描述
protected  boolean  tryAcquire( int  arg ) 獨佔式的獲取同步狀態,獲取成功返回true,否則false
protected  boolean  tryRelease( int  arg ) 獨佔式的釋放同步狀態,釋放成功返回true,否則false
protected  boolean  tryAcquireShared( int  arg ) 共享式的獲取同步狀態,獲取成功返回true,否則false
protected  boolean  tryAcquireShared( int  arg ) 共享式的釋放同步狀態,釋放成功返回true,否則false
protected  boolean  isHeldExclusively(  )

在獨佔模式下,如果當前執行緒已經獲取到同步狀態,則返回true;

其他情況則返回fasle

我們說AQS是一個抽象類,我們以tryAcquire為例看看它在AQS中的實現:

protected boolean tryAcquire(int arg) {
    throw new UnsupportedOperationException();
}

我的天,竟然只是丟擲個異常,這不科學。是的,在AQS中的確沒有實現這個方法,不同的同步工具針對的具體併發場景不同,所以如何獲取同步狀態和如何釋放同步狀態是需要我們在自定義的AQS子類中實現的,如果我們自定義的同步工具需要在獨佔模式下工作,那麼我們就重寫tryAcquiretryReleaseisHeldExclusively方法,如果是在共享模式下工作,那麼我們就重寫tryAcquireSharedtryReleaseShared方法。比如在獨佔模式下我們可以這樣定義一個AQS子類:

public class Sync extends AbstractQueuedSynchronizer {

    @Override
    protected boolean tryAcquire(int arg) {
        return compareAndSetState(0, 1);
    }

    @Override
    protected boolean tryRelease(int arg) {
        setState(0);
        return true;
    }

    @Override
    protected boolean isHeldExclusively() {
        return getState() == 1;
    }
}

tryAcquire表示嘗試獲取同步狀態,我們這裡定義了一種極其簡單的獲取方式,就是使用CAS的方式把state的值設定成1,如果成功則返回true,失敗則返回falsetryRelease表示嘗試釋放同步狀態,這裡同樣採用了一種極其簡單的釋放演算法,直接把state的值設定成0就好了。isHeldExclusively就表示當前是否有執行緒已經獲取到了同步狀態。如果你有更復雜的場景,可以使用更復雜的獲取和釋放演算法來重寫這些方法。

通過上邊的嘮叨,我們只是瞭解了啥是個同步狀態,學會了如何通過繼承AQS來自定義獨佔模式和共享模式下獲取和釋放同步狀態的各種方法,但是你會驚訝的發現會了這些仍然沒有什麼卵用。我們期望的效果是一個執行緒獲取同步狀態成功會立即返回true,並繼續執行某些需要同步的操作,在操作完成後釋放同步狀態,如果獲取同步狀態失敗的話會立即返回false,並且進入阻塞等待狀態,那執行緒是怎麼進入等待狀態的呢?不要走開,下節更精彩。

2. 同步佇列

AQS中還維護了一個所謂的同步佇列,這個佇列的節點類被定義成了一個靜態內部類,它的主要欄位如下

static final class Node {
    volatile int waitStatus;
    volatile Node prev;
    volatile Node next;
    volatile Thread thread;
    Node nextWaiter;

    static final int CANCELLED =  1;
    static final int SIGNAL    = -1;
    static final int CONDITION = -2;
    static final int PROPAGATE = -3;
}

AQS中定義一個頭節點引用,一個尾節點引用

private transient volatile Node head;
private transient volatile Node tail;

通過這兩個節點就可以控制到這個佇列,也就是說可以在佇列上進行諸如插入和移除操作。可以看到Node類中有一個Thread型別的欄位,這表明每一個節點都代表一個執行緒我們期望的效果是當一個執行緒獲取同步狀態失敗之後,就把這個執行緒阻塞幷包裝成Node節點插入到這個同步佇列中,當獲取同步狀態成功的執行緒釋放同步狀態的時候,同時通知在佇列中下一個未獲取到同步狀態的節點,讓該節點的執行緒再次去獲取同步狀態。

這個節點類的其他欄位的意思我們之後遇到會詳細嘮叨,我們先看一下獨佔模式共享模式下在什麼情況下會往這個同步佇列裡新增節點,什麼情況下會從它裡邊移除節點,以及執行緒阻塞和恢復的實現細節。

3. 獨佔式同步狀態獲取與釋放