使用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 的併發工具類