高併發學習之10AQS的理解

1. AQS簡介

在上一篇鎖的認識中我們簡單介紹了一下鎖,已經鎖的基本使用方式,和鎖的兩種重要的實現類ReentrantLock(重入鎖)ReentrantReadWriteLock(讀寫鎖) 的簡單使用,在本篇中,將詳細探討其實現的關鍵步驟AQS(AbstractQueuedSynchronizer)隊列同步器
AQS是用來構建鎖或者其他同步組件的基礎框架,看看我們熟悉的ReentrantLock其源碼:

public class ReentrantLock implements Lock, java.io.Serializable {
    private static final long serialVersionUID = 7373984872572414699L;
    /** Synchronizer providing all implementation mechanics */
    private final Sync sync;

  
    abstract static class Sync extends AbstractQueuedSynchronizer {
    	.....
    }
}

可以看到內部定義了一個抽象類Sync ,實現了AbstractQueuedSynchronizer 。在看下ReentrantReadWriteLock其源碼:

public class ReentrantReadWriteLock
        implements ReadWriteLock, java.io.Serializable {
		abstract static class Sync extends AbstractQueuedSynchronizer {
			.....
		}
}

一樣的,其內部也有個抽象類Sync 繼承了AbstractQueuedSynchronizer 。
Java中AQS的主要使用方式是繼承,子類通過繼承同步器並實現它的抽象方法來管理同步狀態,在抽象方法的實現過程中免不了要對同步狀態進行更改,這時就需要使用同步器提供的3個方法(getState()、setState(int newState)和compareAndSetState(int expect,int update))來進行操作,因爲它們能夠保證狀態的改變是安全的。
子類推薦被定義爲自定義同步組件的靜態內部類,AQS自身沒有實現任何同步接口,它僅僅是定義若干同步狀態獲取和釋放的方法來供自定義同步組件使用,同步器既可以支持獨佔式地獲取同步狀態,也可以支持共享式地獲取同步狀態,這樣就可以方便實現不同類型的同步組件(ReentrantLock、ReentrantReadWriteLock和CountDownLatch等)。
AQS是Java中實現鎖(也可以是任意同步組件)的關鍵,在鎖的實現中聚合AQS,利用AQS實現鎖的語義。
可以這樣理解二者之間的關係:

  • 鎖是面向使用者的,它定義了使用者與鎖交互的接口(比如可以允許兩個線程並行訪問),隱藏了實現細節;
  • 同步器面向的是鎖的實現者,它簡化了鎖的實現方式,屏蔽了同步狀態管理線程的排隊等待與喚醒等底層操作。

鎖和AQS很好地隔離了使用者和實現者所需關注的領域,即使用鎖的人員只需要關注某個類型的鎖他的API是什麼,怎麼使用,而鎖的開發人員需要關注鎖該怎麼實現,提供什麼API。

從使用上來說,AQS的功能可以分爲兩種:獨佔和共享。

  • 獨佔鎖模式下,每次只能有一個線程持有鎖,比如前面給大家演示的ReentrantLock就是以獨佔方式實現的互斥鎖
  • 共享鎖模式下,允許多個線程同時獲取鎖,併發訪問共享資源,比如ReentrantReadWriteLock。

很顯然,獨佔鎖是一種悲觀保守的加鎖策略,它限制了讀/讀衝突,如果某個只讀線程獲取鎖,則其他讀線程都只能等待,這種情況下就限制了不必要的併發性,因爲讀操作並不會影響數據的一致性。共享鎖則是一種樂觀鎖,它放寬了加鎖策略,允許多個執行讀操作的線程同時訪問共享資源。

2. AQS簡單事例

AQS是提供給鎖的開發人員的一種工具,那麼我們現在就利用AQS簡單開發一個自己的鎖。但是在開發之前我們需要去了解AQS爲我們定義了哪些方法。
AQS的設計是基於模板方法模式的,也就是說,使用者需要繼承同步器並重寫指定的方法,隨後將同步器組合在自定義同步組件的實現中,並調用同步器提供的模板方法,而這些模板方法將會調用使用者重寫的方法。
重寫同步器指定的方法時,需要使用同步器提供的如下3個方法來訪問或修改同步狀態。

  • getState():獲取當前同步狀態。
  • setState(int newState):設置當前同步狀態。
  • compareAndSetState(int expect,int update):使用CAS設置當前狀態,該方法能夠保證狀態
    設置的原子性。
    AQS可重寫的方法與描述如下圖所示:
    AQS可重寫的方法
    實現自定義同步組件時,將會調用AQS提供的模板方法,這些(部分)模板方法與描述:
void acquire(int arg);// 獨佔式獲取同步狀態,如果獲取失敗則插入同步隊列進行等待;
void acquireInterruptibly(int arg);// 與acquire方法相同,但在同步隊列中進行等待的時候可以檢測中斷;
boolean tryAcquireNanos(int arg, long nanosTimeout);// 在acquireInterruptibly基礎上增加了超時等待功能,在超時時間內沒有獲得同步狀態返回false;
boolean release(int arg);// 釋放同步狀態,該方法會喚醒在同步隊列中的下一個節點


void acquireShared(int arg);// 共享式獲取同步狀態,與獨佔式的區別在於同一時刻有多個線程獲取同步狀態;
void acquireSharedInterruptibly(int arg);// 在acquireShared方法基礎上增加了能響應中斷的功能;
boolean tryAcquireSharedNanos(int arg, long nanosTimeout);// 在acquireSharedInterruptibly基礎上增加了超時等待的功能;
boolean releaseShared(int arg);// 共享式釋放同步狀態

同步器提供的模板方法基本上分爲3類:

  • 獨佔式獲取與釋放同步狀態
  • -共享式獲取與釋放
  • 同步狀態和查詢同步隊列中的等待線程情況。

自定義同步組件將使用AQS提供的模板方法來實現自己的同步語義。只有掌握了AQS的工作原理才能更加深入地理解併發包中其他的併發組件,所以下面我們開始寫一個獨佔鎖的示例來深入瞭解一下AQS的工作原理。

public class AQSDemo implements Lock {

    private final Sync sync = new Sync();

    private static class Sync extends AbstractQueuedSynchronizer {
        private static final long serialVersionUID = -7590006332728291085L;

        // 返回一個Condition,每個condition都包含了一個condition隊列,後面會分析condition,現在理解成隊列就行
        Condition newCondition() {
            return new ConditionObject();
        }

        // 當狀態爲0的時候獲取鎖
        @Override
        protected boolean tryAcquire(int arg) {
            if (compareAndSetState(0, 1)) {
                setExclusiveOwnerThread(Thread.currentThread());
                return true;
            }
            return false;
        }
        // 釋放鎖,將狀態設置爲0
        @Override
        protected boolean tryRelease(int arg) {
            if (getState() == 0)
                throw new IllegalMonitorStateException();
            setExclusiveOwnerThread(null);
            setState(0);
            return true;
        }

        // 是否處於佔用狀態
        @Override
        protected boolean isHeldExclusively() {
            return getState() == 1;
        }
    }

    /******************* 下面都是實現lock的方法,所有方法的實現都是代理給AQS ************************/
    //獲取鎖
    @Override
    public void lock() {
        sync.acquire(1);
    }
    //獲取鎖的過程能夠響應中斷
    @Override
    public void lockInterruptibly() throws InterruptedException {
        sync.acquireInterruptibly(1);
    }

    //非阻塞式響應中斷能立即返回,獲取鎖放回true反之返回fasle
    @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);
    }
    //獲取與lock綁定的等待通知組件,當前線程必須獲得了鎖才能進行等待,進行等待時會先釋放鎖,當再次獲取鎖時才能從等待中返回
    @Override
    public Condition newCondition() {
        return sync.newCondition();
    }
}
	/******************* 測試是否達到線程安全 **************/
	private static AQSDemo aqsDemo = new AQSDemo();
    private static int count=0;
    public static void main(String[] args) {

        for (int i = 0; i < 100; i++) {
             new Thread(new Runnable() {
                 @Override
                 public void run() {
                     aqsDemo.lock();
                     try {
                         count++;
                     } catch (Exception e) {
                         e.printStackTrace();
                     } finally {
                         aqsDemo.unlock();
                     }
                 }
             } ,"AQSDemo-"+i).start();
        }
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(count);
    }
輸出結果:100

上述示例中,獨佔鎖AQSDemo 是一個自定義同步組件,它在同一時刻只允許一個線程佔有鎖。AQSDemo 中定義了一個靜態內部類,該內部類繼承了AQS並實現了獨佔式獲取和釋放同步狀態。在tryAcquire(int acquires) 方法中,如果經過CAS設置成功(同步狀態設置爲1),則代表獲取了同步狀態,而在 tryRelease(int releases) 方法中只是將同步狀態重置爲0。
用戶使用AQSDemo 時並不會直接和內部同步器的實現打交道,而是調用AQSDemo 提供的方法,在AQSDemo 的實現中,以獲取鎖的lock()方法爲例,只需要在方法實現中調用同步器的模板方法acquire(int args)即可,當前線程調用該方法獲取同步狀態失敗後會被加入到同步隊列中等待,這樣就大大降低了實現一個可靠自定義同步組件的門檻。

3. AQS內部分析

AQS依賴內部的同步隊列(一個FIFO雙向隊列)來完成同步狀態的管理,當前線程獲取同步狀態失時,AQS會將當前線程以及等待狀態等信息構造成爲一個節點(Node)並將其加入同步隊列,同時會阻塞當前線程,當同步狀態釋放時,會把首節點中的線程喚醒,使其再次嘗試獲取同步狀態。
Node的主要屬性如下:

static final class Node {
  int waitStatus; //表示節點的5種狀態
  Node prev; //前繼節點
  Node next; //後繼節點
  Node nextWaiter; //存儲在condition隊列中的後繼節點
  Thread thread; //當前線程
}

其中節點狀態說明:

  • Cancelled,值爲1,由於在同步隊列中等待的線程等待超時或者被中斷,需要從同步隊
    列中取消等待,節點進入該狀態將不會變化
  • Signal,值爲-1,後繼節點的線程處於等待狀態,而當前節點的線程如果釋放了同步狀
    態或者被取消,將會通知後繼節點,使後繼節點的線程得以運行
  • Condition,值爲-2,節點在等待隊列中,節點線程等待在Condition 上,當其他線程對
    Condition 調用了 signal()方法後,該節點將會從等待隊列中轉移到同步隊列中,加入到對同步狀
    態的獲取中
  • Propagate, 值爲-3, 表示下一次共享式同步狀態獲取將會無條件地被傳播下去
  • Initial, 值爲0,初始狀態

節點是構成同步隊列(等待隊列,後面文章中將會介紹)的基礎,AQS擁有首節點(head)和尾節點(tail),沒有成功獲取同步狀態的線程將會成爲節點加入該隊列的尾部。

通過上面AQSDemo例子中我們將i調整成10,dug調試如圖:
AQS結構圖
可以看出包含sync包含頭尾兩個節點屬性,在看Node屬性如圖:
AQS結構圖
每個節點都有prev(前置節點)、next(下一個節點),並且每個節點用來保存獲取同步狀態失敗的線程引用以及等待狀態(waitStatus)等信息。
下面是AQS結構如圖:
AQS的基本結構
AQS包含了兩個節點類型的引用,一個指向頭節點,而另一個指向尾節點。試想一下,當一個線程成功地獲取了同步狀態(或者鎖),其他線程將無法獲取到同步狀態,轉而被構造成爲節點並加入到同步隊列中,而這個加入隊列的過程必須要保證線程安全,因此AQS提供了一個基於CAS的設置尾節點的方法:compareAndSetTail(Node expect,Node update),它需要傳遞當前線程“認爲”的尾節點和當前節點,只有設置成功後,當前節點才正式與之前的尾節點建立關聯。
AQS將節點加入到同步隊列的過程如圖:
節點加入到同步隊列
同步隊列遵循FIFO,首節點是獲取同步狀態成功的節點,首節點的線程在釋放同步狀態時,將會喚醒後繼節點,而後繼節點將會在獲取同步狀態成功時將自己設置爲首節點,該過程如圖:
首節點的設置
設置首節點是通過獲取同步狀態成功的線程來完成的,由於只有一個線程能夠成功獲取到同步狀態,因此設置頭節點的方法並不需要使用CAS來保證,它只需要將首節點設置成爲原首節點的後繼節點並斷開原首節點的next引用即可。

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

通過調用同步器的acquire(int arg)方法可以獲取同步狀態,該方法對中斷不敏感,也就是由於線程獲取同步狀態失敗後進入同步隊列中,後續對線程進行中斷操作時,線程不會從同步隊列中移出,該方法源碼如下,注意看註釋:

 public final void acquire(int arg) {
 		//當前是否獲取到同步狀態,如果沒有這將CAS自旋將當前節點設置成尾節點,並自旋獲取同步狀態,如果獲取成功則退出自旋,並喚醒當前線程。
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }
//自旋獲取同步狀態,直到被前置節點喚醒或則被中斷
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;
               }
               //檢查和更新未能獲取的節點的狀態,如果前置節點已釋放,且當前線程被中斷
               if (shouldParkAfterFailedAcquire(p, node) &&
                   parkAndCheckInterrupt())
                   interrupted = true;
           }
       } finally {
           if (failed)//取消獲取同步狀態
               cancelAcquire(node);
       }
   }
  //中斷當前線程
static void selfInterrupt() {
        Thread.currentThread().interrupt();
    }
/**
檢查和更新未能獲取同步狀態的節點的狀態。如果線程阻塞則返回true,
**/
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
		//前置節點狀態
        int ws = pred.waitStatus;
        // 如果前置節點已釋放同步狀態
        if (ws == Node.SIGNAL) return true;
        //如果前置節點被中斷,設置前置節點的前置節點爲當前節點的前置節點,
        //否者自旋設置前置節點狀態爲SIGNAL(-1)
        if (ws > 0) {
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
        	  /*
             * 	等待狀態爲0或者-3時 需要將當前節點狀態設爲-1,表示前置節點釋放後當前節點釋放
             */
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }
//阻塞當前線程,確認是否被中斷
private final boolean parkAndCheckInterrupt() {
      LockSupport.park(this);
      return Thread.interrupted();
  }
//等待隊列中的尾節點,被volatile 修飾,多線程可見
private transient volatile Node tail;
//生成同步節點,當前節點準備變爲等待隊列中尾節點
private Node addWaiter(Node mode) {
    Node node = new Node(Thread.currentThread(), mode);
    // 快速嘗試在尾部添加
    Node pred = tail;
    if (pred != null) {
        node.prev = pred;
        //compareAndSetTail(Node expect,Node update)方法來確保節點能夠被線程安全添加
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    //自旋直到自己被設置成尾節點
    enq(node);
    return node;
}
//自旋直到自己被設置成尾節點
private Node enq(final Node node) {
        for (;;) {
        	//獲取最新的尾結點
            Node t = tail;
            if (t == null) { // Must initialize
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {
                node.prev = t;
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
    }

上述代碼主要完成了同步狀態獲取、節點構造、加入同步隊列以及在同步隊列中自旋等待的相關工作,其主要邏輯是:

  • 首先調用自定義同步器實現的tryAcquire(int arg)方法,該方法保證線程安全的獲取同步狀態(上面demo中我們重寫了該方法)
  • 如果同步狀態獲取失敗,則構造同步節點(獨佔式Node.EXCLUSIVE,同一時刻只能有一個線程成功獲取同步狀態)
  • 通過addWaiter(Node node)方法將該節點加入到同步隊列的尾部,
  • 最後調用acquireQueued(Node node,int arg)方法,使得該節點以“死循環”的方式獲取同步狀態。
  • 如果獲取不到則阻塞節點中的線程,而被阻塞線程的喚醒主要依靠前驅節點的出隊或阻塞線程被中斷來實現。

在addWaiter中:通過使用compareAndSetTail(Node expect,Node update)方法來確保節點能夠被線程安全添加。試想一下:如果使用一個普通的LinkedList來維護節點之間的關係,那麼當一個線程獲取了同步狀態,而其他多個線程由於調用tryAcquire(int arg)方法獲取同步狀態失敗而併發地被添加到LinkedList時,LinkedList將難以保證Node的正確添加,最終的結果可能是節點的數量有偏差,而且順序也是混亂的。
在**enq(final Node node)方法中,同步器通過“死循環”來保證節點的正確添加,在“死循環”中只有通過CAS將節點設置成爲尾節點之後,當前線程才能從該方法返回,否則,當前線程不斷地嘗試設置。可以看出,enq(final Node node)方法將併發添加節點的請求通過CAS變得“串行化”了。
節點進入AQS之後,就進入了一個自旋的過程,每個節點(或者說每個線程)都在自省地觀察,當條件滿足,獲取到了同步狀態,就可以從這個自旋過程中退出,否則依舊留在這個自旋過程中(並會阻塞節點的線程)。
acquireQueued(final Node node,int arg)**方法中,當前線程在“死循環”中嘗試獲取同步狀態,而只有前驅節點是頭節點才能夠嘗試獲取同步狀態,這樣做的原因有兩個:

  • 頭節點是成功獲取到同步狀態的節點,而頭節點的線程釋放了同步狀態之後,將會喚醒其後繼節點,後繼節點的線程被喚醒後需要檢查自己的前驅節點是否是頭節點。
  • 維護同步隊列的FIFO原則。

該方法中,節點自旋獲取同步狀態的行爲如圖:
節點自旋獲取同步狀態
由於非首節點線程前驅節點出隊或者被中斷而從等待狀態返回,隨後檢查自己的前驅是否是頭節點,如果是則嘗試獲取同步狀態。可以看到節點和節點之間在循環檢查的過程中基本不相互通信,而是簡單地判斷自己的前驅是否爲頭節點,這樣就使得節點的釋放規則符合FIFO,並且也便於對過早通知的處理(過早通知是指前驅節點不是頭節點的線程由於中斷而被喚醒)。
獨佔式同步狀態獲取流程,也就是acquire(int arg)方法調用流程:
獨佔式同步狀態獲取流程
前驅節點爲頭節點且能夠獲取同步狀態的判斷條件和線程進入等待狀態是獲取同步狀態的自旋過程。當同步狀態獲取成功之後,當前線程從acquire(int arg)方法返回,如果對於鎖這種併發組件而言,代表着當前線程獲取了鎖。
當前線程獲取同步狀態並執行了相應邏輯之後,就需要釋放同步狀態,使得後續節點能夠繼續獲取同步狀態。通過調用AQS的**release(int arg)**方法可以釋放同步狀態,該方法在釋放了同步狀態之後,會喚醒其後繼節點(進而使後繼節點重新嘗試獲取同步狀態)。代碼如下:

public final boolean release(int arg) {
	if (tryRelease(arg)) {
		Node h = head;
		if (h != null && h.waitStatus != 0)
		unparkSuccessor(h);
		return true;
	}
	return false;
}

該方法執行時,會喚醒頭節點的後繼節點線程,unparkSuccessor(Node node)方法使用
LockSupport 線程間通信來喚醒處於等待狀態的線程。

總結:在獲取同步狀態時,AQS維護一個同步隊列,獲取狀態失敗的線程都會被加入到隊列中並在隊列中進行自旋;移出隊列(或停止自旋)的條件是前驅節點爲頭節點且成功獲取了同步狀態。在釋放同步狀態時,AQS調用tryRelease(int arg)方法釋放同步狀態,然後喚醒頭節點的後繼節點。

5. 共享式同步狀態獲取與釋放

共享式獲取與獨佔式獲取最主要的區別在於同一時刻能否有多個線程同時獲取到同步狀態。以文件的讀寫爲例,如果一個程序在對文件進行讀操作,那麼這一時刻對於該文件的寫操作均被阻塞,而讀操作能夠同時進行。寫操作要求對資源的獨佔式訪問,而讀操作可以是共享式訪問,兩種不同的訪問模式在同一時刻對文件或資源的訪問情況,如圖:
共享式與獨佔式訪問資源的對比
在圖中,左半部分,共享式訪問資源時,其他共享式的訪問均被允許,而獨佔式訪問被阻塞,右半部分是獨佔式訪問資源時,同一時刻其他訪問均被阻塞。
通過調用同步器的acquireShared(int arg)方法可以共享式地獲取同步狀態,該方法源碼如下:

public final void acquireShared(int arg) {
        if (tryAcquireShared(arg) < 0)
            doAcquireShared(arg);
    }
private void doAcquireShared(int arg) {
        final Node node = addWaiter(Node.SHARED);
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                final Node p = node.predecessor();
                if (p == head) {
                    int r = tryAcquireShared(arg);
                    if (r >= 0) {
                        setHeadAndPropagate(node, r);
                        p.next = null; // help GC
                        if (interrupted)
                            selfInterrupt();
                        failed = false;
                        return;
                    }
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

在acquireShared(int arg)方法中,同步器調用tryAcquireShared(int arg)方法嘗試獲取同步狀態,tryAcquireShared(int arg)方法返回值爲int類型,當返回值大於等於0時,表示能夠獲取到同步狀態。因此,在共享式獲取的自旋過程中,成功獲取到同步狀態並退出自旋的條件就是tryAcquireShared(int arg)方法返回值大於等於0。可以看到,在doAcquireShared(int arg)方法的自旋過程中,如果當前節點的前驅爲頭節點時,嘗試獲取同步狀態,如果返回值大於等於0,表示該次獲取同步狀態成功並從自旋過程中退出。
與獨佔式一樣,共享式獲取也需要釋放同步狀態,通過調用releaseShared(int arg)方法可以
釋放同步狀態,該方法源碼如下:

public final boolean releaseShared(int arg) {
        if (tryReleaseShared(arg)) {
            doReleaseShared();
            return true;
        }
        return false;
    }

該方法在釋放同步狀態之後,將會喚醒後續處於等待狀態的節點。對於能夠支持多個線程同時訪問的併發組件(比如Semaphore),它和獨佔式主要區別在於tryReleaseShared(int arg)方法必須確保同步狀態(或者資源數)線程安全釋放,一般是通過循環和CAS來保證的,因爲釋放同步狀態的操作會同時來自多個線程。

6. 獨佔式超時獲取同步狀態

通過調用AQS的doAcquireNanos(int arg,long nanosTimeout)方法可以超時獲取同步狀態,即在指定的時間段內獲取同步狀態,如果獲取到同步狀態則返回true,否則,返回false。該方法提供了傳統Java同步操作(比如synchronized關鍵字)所不具備的特性。
在分析該方法的實現前,先介紹一下響應中斷的同步狀態獲取過程。在Java 5之前,當一個線程獲取不到鎖而被阻塞在synchronized之外時,對該線程進行中斷操作,此時該線程的中斷標誌位會被修改,但線程依舊會阻塞在synchronized上,等待着獲取鎖。在Java 5中,同步器提供了acquireInterruptibly(int arg)方法,這個方法在等待獲取同步狀態時,如果當前線程被中斷,會立刻返回,並拋出InterruptedException。
超時獲取同步狀態過程可以被視作響應中斷獲取同步狀態過程的“增強版”,doAcquireNanos(int arg,long nanosTimeout)方法在支持響應中斷的基礎上,增加了超時獲取的特性。針對超時獲取,主要需要計算出需要睡眠的時間間隔nanosTimeout,爲了防止過早通知,nanosTimeout計算公式爲:
nanosTimeout-=now-lastTime,
其中now爲當前喚醒時間,lastTime爲上次喚醒時間,如果nanosTimeout大於0則表示超時時間未到,需要繼續睡眠nanosTimeout納秒,反之,表示已經超時,該方法源碼如下:

 private boolean doAcquireNanos(int arg, long nanosTimeout)
            throws InterruptedException {
        if (nanosTimeout <= 0L)
            return false;
        final long deadline = System.nanoTime() + nanosTimeout;
        final Node node = addWaiter(Node.EXCLUSIVE);
        boolean failed = true;
        try {
            for (;;) {
                final Node p = node.predecessor();
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return true;
                }
                //計算時間,當前時間now減去睡眠之前的時間lastTime得到已經睡眠
				//的時間delta,然後被原有超時時間nanosTimeout減去,得到了
				//還應該睡眠的時間
                nanosTimeout = deadline - System.nanoTime();
                if (nanosTimeout <= 0L)
                    return false;
                if (shouldParkAfterFailedAcquire(p, node) &&
                    nanosTimeout > spinForTimeoutThreshold)
                    LockSupport.parkNanos(this, nanosTimeout);
                if (Thread.interrupted())
                    throw new InterruptedException();
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

該方法在自旋過程中,當節點的前驅節點爲頭節點時嘗試獲取同步狀態,如果獲取成功則從該方法返回,這個過程和獨佔式同步獲取的過程類似,但是在同步狀態獲取失敗的處理上有所不同。如果當前線程獲取同步狀態失敗,則判斷是否超時(nanosTimeout小於等於0表示已經超時),如果沒有超時,重新計算超時間隔nanosTimeout,然後使當前線程等待nanosTimeout納秒(當已到設置的超時時間,該線程會從LockSupport.parkNanos(Objectblocker,long nanos)方法返回)。
如果nanosTimeout小於等於spinForTimeoutThreshold(1000納秒)時,將不會使該線程進行超時等待,而是進入快速的自旋過程。原因在於,非常短的超時等待無法做到十分精確,如果這時再進行超時等待,相反會讓nanosTimeout的超時從整體上表現得反而不精確。因此,在超時非常短的場景下,同步器會進入無條件的快速自旋。
獨佔式超時獲取同步態的流程如圖:獨佔式超時獲取同步狀態的流程
從圖可以看出,獨佔式超時獲取同步狀態doAcquireNanos(int arg,long nanosTimeout)和獨佔式獲取同步狀態acquire(int args)在流程上非常相似,其主要區別在於未獲取到同步狀態時的處理邏輯。acquire(int args)在未獲取到同步狀態時,將會使當前線程一直處於等待狀態,而doAcquireNanos(int arg,long nanosTimeout)會使當前線程等待nanosTimeout納秒,如果當前線程在nanosTimeout納秒內沒有獲取到同步狀態,將會從等待邏輯中自動返回。

7. 簡單的Demo

爲了加深影響我們設計一個同步工具:該工具在同一時刻,只允許至多兩個線程同時訪問,超過兩個線程的訪問將被阻塞,我們將這個同步工具命名爲TwinsLock。

  • 首先,確定訪問模式。TwinsLock能夠在同一時刻支持多個線程的訪問,這顯然是共享式訪問,因此,需要使用AQS提供的acquireShared(int args)方法等和Shared相關的方法,這就要求TwinsLock必須重寫tryAcquireShared(int args)方法和tryReleaseShared(int args)方法,這樣才能保證AQS的共享式同步狀態的獲取與釋放方法得以執行。
  • 其次,定義資源數。TwinsLock在同一時刻允許至多兩個線程的同時訪問,表明同步資源數爲2,這樣可以設置初始狀態status爲2,當一個線程進行獲取,status減1,該線程釋放,則status加1,狀態的合法範圍爲0、1和2,其中0表示當前已經有兩個線程獲取了同步資源,此時再有其他線程對同步狀態進行獲取,該線程只能被阻塞。在同步狀態變更時,需要使用compareAndSet(int expect,int update)方法做原子性保障。
  • 最後,組合自定義同步器。
    前面提到,自定義同步組件通過組合自定義同步器來完成同步功能,一般情況下自定義同步器會被定義爲自定義同步組件的內部類。
public class TwinsLock implements Lock {
    private final Sync sync = new Sync(2);

    private static final class Sync extends AbstractQueuedSynchronizer {
        private static final long serialVersionUID = -6008501609713522064L;

        Sync(int count) {
            if (count <= 0) {
                throw new IllegalArgumentException("count must large than zero.");
            }
            setState(count);
        }

        public int tryAcquireShared(int reduceCount) {
            for (; ; ) {
                int current = getState();
                int newCount = current - reduceCount;
                if (newCount < 0 || compareAndSetState(current, newCount)) {
                    return newCount;
                }
            }
        }

        public boolean tryReleaseShared(int returnCount) {
            for (; ; ) {
                int current = getState();
                int newCount = current + returnCount;
                if (compareAndSetState(current, newCount)) {
                    return true;
                }
            }
        }
    }

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

    @Override
    public void lockInterruptibly() throws InterruptedException {

    }

    @Override
    public boolean tryLock() {
        return sync.releaseShared(1);
    }

    @Override
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
        return false;
    }

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

    @Override
    public Condition newCondition() {
        return null;
    }
}

在上述示例中,TwinsLock實現了Lock接口,提供了面向使用者的接口,使用者調用lock()方法獲取鎖,隨後調用unlock()方法釋放鎖,而同一時刻只能有兩個線程同時獲取到鎖。
TwinsLock同時包含了一個自定義同步器Sync,而該同步器面向線程訪問和同步狀態控制。以共享式獲取同步狀態爲例:同步器會先計算出獲取後的同步狀態,然後通過CAS確保狀態的正確設置,當**tryAcquireShared(int reduceCount)**方法返回值大於等於0時,當前線程才獲取同步狀態,對於上層的TwinsLock而言,則表示當前線程獲得了鎖。
AQS作爲一個橋樑,連接線程訪問以及同步狀態控制等底層技術與不同併發組件(比如Lock、CountDownLatch等)的接口語義。
下面編寫一個測試來驗證TwinsLock是否能按照預期工作。在測試用例中,定義了工作者
線程Worker,該線程在執行過程中獲取鎖,當獲取鎖之後使當前線程睡眠1秒(並不釋放鎖),
隨後打印當前線程名稱,最後再次睡眠1秒並釋放鎖,測試用例如下所示:

public class TwinsLockTest {

    public static void main(String[] args) throws InterruptedException {
        final Lock lock = new TwinsLock();
        // 啓動10個線程
        for (int i = 0; i < 10; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        lock.lock();
                        Thread.sleep(1000);
                        System.out.println(Thread.currentThread().getName());
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }finally {
                        lock.unlock();
                    }

                }
            }, "Thread-" + i).start();
        }
        for (int i = 0; i < 30; i++) {
            Thread.sleep(1000);
            System.out.println();
        }
    }
}

運行該測試用例,可以看到線程名稱成對輸出,也就是在同一時刻只有兩個線程能夠獲
取到鎖,這表明TwinsLock可以按照預期正確工作。

輸出結果如下:
Thread-0
Thread-2


Thread-1
Thread-3


Thread-5
Thread-4


Thread-7
Thread-6


Thread-9
Thread-8

8. 總結

總結一下:

  • AQS是用來構建鎖或者其他同步組件的基礎框架,它使用了一個int成員變量表示同步狀態,
  • AQS既可以支持獨佔式地獲取同步狀態,也可以支持共享式地獲取同步狀態,這樣就可以方便實現不同類型的同步組件。
  • 同步器是實現鎖(也可以是任意同步組件)的關鍵,在鎖的實現中聚合同步器,利用同步
    器實現鎖的語義。可以這樣理解二者之間的關係:鎖是面向使用者的,它定義了使用者與鎖交
    互的接口(比如可以允許兩個線程並行訪問),隱藏了實現細節;同步器面向的是鎖的實現者,
    它簡化了鎖的實現方式,屏蔽了同步狀態管理、線程的排隊、等待與喚醒等底層操作。鎖和同
    步器很好地隔離了使用者和實現者所需關注的領域。
    其實上面的就是剛開始的簡介,現在看是不是理解深刻。。。

參考文獻 《Java併發編程的藝術》

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章