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
子類中實現的,如果我們自定義的同步工具需要在獨佔模式
下工作,那麼我們就重寫tryAcquire
、tryRelease
和isHeldExclusively
方法,如果是在共享模式
下工作,那麼我們就重寫tryAcquireShared
和tryReleaseShared
方法。比如在獨佔模式下我們可以這樣定義一個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
,失敗則返回false
,tryRelease
表示嘗試釋放同步狀態
,這裡同樣採用了一種極其簡單的釋放演算法,直接把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
節點插入到這個同步佇列
中,當獲取同步狀態成功的執行緒釋放同步狀態的時候,同時通知在佇列中下一個未獲取到同步狀態的節點,讓該節點的執行緒再次去獲取同步狀態。
這個節點類
的其他欄位的意思我們之後遇到會詳細嘮叨,我們先看一下獨佔模式
和共享模式
下在什麼情況下會往這個同步佇列
裡新增節點,什麼情況下會從它裡邊移除節點,以及執行緒阻塞和恢復的實現細節。