Java併發編程實戰(學習筆記 十三 第十四章 構建自定義的同步工具 下 )

14.3 顯式的Condition對象

13章中介紹,在某些情況下,當內置鎖過於靈活時,可以使用顯式鎖。
正如Lock時一種廣義的內置鎖,Condition時一種廣義的內置條件隊列

//       14-10     Condition接口
public interface Condition {
   void await() throws InterruptedException;
   boolean await(long time, TimeUnit unit)
                throws InterruptedException;
   long awaitNanos(long nanosTimeout) throws InterruptedException;                               
   void awaitUninterruptibly();
   boolean awaitUntil(Date deadline) throws InterruptedException;
   void signal();
   void signalAll();
}

內置條件隊列存在一些缺陷,每個內置鎖都只能有一個相關聯的條件隊列,因而在像BoundedBuffer這種類中,多個線程可能在同一個條件隊列上等待不同的條件謂詞,並在最常見的加鎖模式下公開條件隊列對象。

如果想編寫一個帶有多個條件謂詞的併發對象,或者想獲得除了條件隊列可見性的更多控制權,就可以使用顯式的Lock和Condition而不是內置鎖和條件隊列。

一個Condition和一個Lock關聯在一起,就像一個條件隊列和一個內置鎖相關聯一樣。要創建一個Condition,可以在相關聯的Lock上調用Lock.newCondition方法。
Condition比內置條件隊列提供了更豐富的功能:在每個鎖上可存在多個等待,條件等待可以時可中斷的或不可中斷的,基於時限的等待,以及公平的非公平的隊列操作。

與內置條件隊列不同的時,對於每個Lock,可以有任意數量的Condition對象。Condition對象繼承了相關的Lock對象的公平性,對於公平的鎖,線程會依照FIFO順序從Condition.await中釋放。

在Condition對象,與wait,notify,notifyAll方法分別對應的時await,signal,signalAll。但是Condition對Object進行了擴展,因而它也包含wait和notify方法,一定要確保使用正確的版本——await和signal

14-11使用兩個Condition,分別爲notFull和notEmpty,用於表示”非空“和”非滿“兩個條件謂詞。當緩存爲空時,take將阻塞並等待notEmpty,此時put想notEmpty發送信號,可以解除任何在take中阻塞的線程。

//     14-11  使用顯式條件變量的有界緩存
@ThreadSafe
public class ConditionBoundedBuffer <T> {
    protected final Lock lock = new ReentrantLock();
    //條件謂詞:notFull(count<items.length)
    private final Condition notFull = lock.newCondition();
  //條件謂詞:noEmpty(count>0)
    private final Condition notEmpty = lock.newCondition();
    private static final int BUFFER_SIZE = 100;
    @GuardedBy("lock") private final T[] items = (T[]) new Object[BUFFER_SIZE];
    @GuardedBy("lock") private int tail, head, count;

    //阻塞直到notFull
    public void put(T x) throws InterruptedException {
        lock.lock();
        try {
            while (count == items.length)
                notFull.await();
            items[tail] = x;
            if (++tail == items.length)
                tail = 0;
            ++count;
            notEmpty.signal();//put想notEmpty發送信號,可以解除任何在take中阻塞的線程。
        } finally {
            lock.unlock();
        }
    }

  //阻塞直到notEmpty
    public T take() throws InterruptedException {
        lock.lock();
        try {
            while (count == 0)
                notEmpty.await();
            T x = items[head];
            items[head] = null;
            if (++head == items.length)
                head = 0;
            --count;
            notFull.signal();
            return x;
        } finally {
            lock.unlock();
        }
    }
}

在分析使用多個Condition的類時,比分析一個使用單一內部隊列加多個條件謂詞的類簡單得多。通過將兩個條件謂詞分開並放到兩個等待線程集中,Condition使其更容易滿足單次通知的需求。
signal比signalAll更高效,它嫩極大地減少在每次緩存操作中發生的上下文切換與鎖請求的次數。

當使用顯式的Lock和Condition時,也必須滿足鎖,條件謂詞和條件變量之間的三元關係。

如果需要一些高級功能,例如使用公平的隊列操作或者在每個鎖上對應多個等待線程集,那麼應該優先使用Condition而不是內置條件隊列。


14.4 Synchronizer(同步器)分析

ReentrantLock和Semaphore這兩個接口之間存在許多共同點,這兩個類都已用做一個“閥門”,即每次只允許一定數量的線程通過,並當線程到達閥門時,可以通過(在調用lock或acquire時成功返回),也可以等待(在調用lock或acquire時阻塞),還可以取消(在調用tryLock或tryAuquire時返回”假”,表示在指定的時間內鎖時不可用的或無法獲得許可)。
而且,這兩個接口都支持可中斷的,不可中斷的以及限時的獲取操作,也支持等待線程執行公平或非公平的隊列操作。

我們可以用鎖來實現計數信號量,以及可以通過計數信號量來實現鎖

這並非java.util.concurrent.Semaphore的真實現方式

//         14-12  使用Lock來實現信號量
@ThreadSafe
public class SemaphoreOnLock {
    private final Lock lock=new ReentrantLock();
    //條件謂詞:permitsAvailable(permits>0)
    private final Condition permitsAvailable=lock.newCondition();
    private int permits;

   SemaphoreOnLock(int initialPermits) {
        lock.lock();
        try{
            permits=initialPermits;
        }finally {
            lock.unlock();
        }
    }
   //阻塞直到:permitsAvailable
   public void acquire() throws InterruptedException{
       lock.lock();
       try{
           while(permits<=0)
               permitsAvailable.await();
           --permits;
       }finally{
           lock.unlock();
       }
   }
   public void release(){
       lock.lock();
       try{
           ++permits;
           permitsAvailable.signal();
       }finally {
        lock.unlock();
    }
   }
}

事實上,ReentrantLock和Semaphore都使用了一個共同的基類,即AbstractQueueSynchronizer(AQS),這個類也是其他許多同步類的基類。

AQS是一個用於構建鎖和同步器的框架,許多同步器都可以通過AQS很容易並且高效地構造出來。

AQS解決了在實現同步器時設計的大量細節問題,例如等待線程採用FIFO隊列操作順序。在不同的同步器還可以定義一些靈活的標準來判斷某個線程是應該通過還是等待。

基於AQS來構建同步器能帶來許多好處。它不僅能極大地減少實現工作,而且也不必處理在多個位置上的競爭問題。


14.5 AbstractQueueSynchronizer

多數情況下不會直接使用AQS,標準同步器類集合能滿足絕大多數情況的需求。

在基於AQS構建的同步器類中,最基本的操作包括各種形式的獲取操作和釋放操作。

獲取操作是一種依賴狀態的操作,並且通常會阻塞。
在使用鎖或信號量時,獲取(acquire)操作獲得的是鎖或許可,並且調用者可能會一直等待直到同步器類處於可被獲取的狀態。
在使用CountDownLatch時,獲取(countDown)操作意味這“等待並直到閉鎖到達結束狀態”
使用FutureTask時,獲取(get)操作意味着“等待並直到任務已經完成”。

釋放並不是一個可阻塞的操作,當執行釋放操作時,所有在請求時被阻塞的線程都會開始執行。

如果一個類想成爲狀態依賴的類,必須擁有一些狀態。
AQS負責管理同步器類中的狀態,它管理了一個整數狀態信息,可以通過getState,setState以及compareAndSetState等protected類型方法來進行操作。
ReentrantLock用它來表示所有者線程已經重複獲取該鎖的次數。
Semaphore用它表示剩餘的許可數量(acquire和release操作)。
FutureTask用它來表示任務的狀態(尚未開始,正在運行,以完成以及已取消)。
還可以自行管理一些額外的狀態變量,例如,ReentrantLock保存了鎖的當前所有者的信息,這樣就能區分某個操作是重入還是競爭的。

下面給出了AQS中的獲取操作與釋放操作的形式。
根據同步器的不同,獲取操作可以是一種獨佔操作(例如ReentrantLock)也可以是一個非獨佔操作(例如Semaphore和CountDownLatch)。

//        14-13   AQS中獲取操作與釋放操作的標準形式
boolean acquire() throws InterruptedException {
    while (當前狀態不允許獲取操作) {
       if (需要阻塞獲取請求) {
            如果當前線程不在隊列中,則將其插入隊列
            阻塞當前線程
       }
       else
           返回失敗
    }
    可能更新同步器的狀態
    如果線程處於隊列中,則將其移出隊列
    返回成功
}
void release() {
    更新同步器的狀態
    if (新的狀態允許某個被阻塞的線程獲取成功)
       解除隊列中一個或多個線程阻塞狀態
}

如果某個同步器支持獨佔的獲取操作,那麼需要實現一些保護方法,包括tryAcquire,tryRelease和isHeldExclusively等
對於支持共享獲取的同步器,則應該實現tryAuquireShared和tryReleaseShared。
AQS中的acquire, acquireShared, release和releaseShared等方法都將調用這些方法在子類中帶有前綴try的版本來判斷某個操作是否能執行。

在同步器的子類中,可以根據其獲取操作和釋放操作的語義,使用getState,setState以及compareAndSetState來檢查和更新狀態,並通過返回的狀態值來告知基類“獲取”或“釋放”同步器的操作是否成功。

14-14是一個使用AQS實現的二元閉鎖。它包含兩個公有方法:await和signal,分別對應獲取操作和釋放操作。
起初,閉鎖是關閉的,任何調用await的線程都將阻塞並直到閉鎖被打開。當通過調用signal打開閉鎖時,所有等待中的線程都將被釋放,並且隨後到達閉鎖的線程也被允許執行。

//      14-14   使用AbstractQueueSynchronizer實現的二元鎖
@ThreadSafe
public class OneShotLatch {
    private final Sync sync=new Sync();
    //AQS中的acquire, acquireShared, release和releaseShared等方法都將調用這些方法在子類中帶有前綴try的版本來判斷某個操作是否能執行。
    public void signal(){
        sync.releaseShared(0);
    }
    //起初,閉鎖是關閉的(0),任何調用await的線程都將阻塞並直到閉鎖被打開
    public void await() throws InterruptedException {
        sync.acquireSharedInterruptibly(0);
    }

    private class Sync extends AbstractQueuedSynchronizer{
        protected int tryAcquireShared(int ignored){
            //如果閉鎖時打開的(state==1),那麼這個操作將成功,否則將失敗
            return (getState()==1)?1:-1;
        }
        //在tryReleaseShared中將閉鎖狀態設置爲打開,(通過返回值)表示該同步器處於完全被釋放的狀態,因而AQS讓所有等待中的線程都重新嘗試請求該同步器,並且由於tryReleaseShared將返回成功,因此現在的請求操作將成功
        protected boolean tryReleaseShared(int ignored){
            setState(1);  //現在打開閉鎖
            return true;   //現在其他的線程可以獲取該閉鎖
        }
    }
}

在OneShotLatch中,AQS狀態用來表示閉鎖狀態——關閉(0)或打開(1)。await方法調用AQS的acquireSharedInterruptibly,然後接着調用OneShotLatch中的tryAcquireShared方法。
在tryAcquireShared的實現中必須返回一個值來表示這個該獲取操作能否執行。
如果之前已經打開了閉鎖,那麼tryAcquireShared將返回成功並允許線程通過,否則就會返回一個表示獲取操作失敗的值。
acquireSharedInterruptibly處理失敗的方式,是把這個線程放入等待隊列中。
類似地,signal將調用releaseShared,接下來又會調用tryReleaseShared。
在tryReleaseShared中將閉鎖狀態設置爲打開,(通過返回值)表示該同步器處於完全被釋放的狀態,因而AQS讓所有等待中的線程都重新嘗試請求該同步器,並且由於tryReleaseShared將返回成功,因此現在的請求操作將成功。


14.6 java.util.concurrent同步器類中的AQS

java.util.concurrent中的許多阻塞類,例如ReentrantLock,Semaphore,ReentrantReadWriteLock,CountDownLatch,SynchronousQueue和FutureTask等,都是基於AQS構建的。

14.6.1 ReentrantLock

ReentrantLock只支持獨佔方式的獲取操作,因此它實現了tryAcquire,tryRelease和isHeldExclusively,14-15給出了非公平版本的tryAcquire。ReentrantLock將同步狀態用於保存鎖獲取操作的次數,並且還維護一個owner變量來保存當前所有者線程的標識符,只有在當前線程剛剛獲取到鎖,或者正要釋放鎖額時候,纔會修改這個變量。
在tryRelease中檢查owner域,從而確保當前線程正在執行unlock操作之前已經獲取了鎖:在tryAcquire中將使用這個域來區分獲取操作時重入的還是競爭的

//      14-15  基於非公平的ReentrantLock實現tryAcquire
protected boolean tryAcquire(int ignored) {
    final Thread current = Thread.currentThread();
    int c = getState();
    //如果鎖未被持有,那麼它將嘗試更新鎖的狀態以表示鎖已經被持有。
    if (c == 0) {
      //使用compareAndSetState來原子地更新狀態,表示這個鎖已經被佔有,並確保狀態在最後一次檢查以後就沒有被修改過
      if (compareAndSetState(0, 1)) {
        owner = current;
        return true;    //(通過返回值)表示該同步器處於完全被釋放的狀態
      }
    } else if (current == owner) {//如果鎖狀態表明它已經被持有,並且如果當前線程是鎖的擁有者,那麼獲取計數會遞增,如果當前線程不是鎖的擁有者,那麼獲取操作將失敗
       setState(c+1);
       return true;
    }
    return false;
}

在一個線程嘗試獲取鎖時,tryAcquire將首先檢查鎖的狀態。
如果鎖未被持有,那麼它將嘗試更新鎖的狀態以表示鎖已經被持有。

由於狀態可能在檢查後被立即修改,因此tryAcquire使用compareAndSetState來原子地更新狀態,表示這個鎖已經被佔有,並確保狀態在最後一次檢查以後就沒有被修改過。

根據15.2可以知道,如果c的值在這個過程中沒有被修改,仍爲0,則變爲1,表示這個鎖已經被佔有,並返回true,否則返回false。這是爲了避免狀態在檢查後立即被修改。

如果鎖狀態表明它已經被持有,並且如果當前線程是鎖的擁有者,那麼獲取計數會遞增,如果當前線程不是鎖的擁有者,那麼獲取操作將失敗。

ReentrantLock還利用了AQS對多個條件變量和多個等待線程集的內置支持。Lock.newCondition將返回一個新的ConditionObject實例,這是AQS的一個內部類。

14.6.2 Semaphore與CountDownLatch

Semaphore將AQS的同步狀態用於保存當前可用許可的數量。
14-16中的tryAcquireShared方法首先計算剩餘許可的數量,如果沒有足夠的許可,那麼會返回一個值表示獲取操作的失敗。
如果還有剩餘的許可,那麼tryAcquireShared會通過compareAndSetState以原子方式來降低許可的計數。
如果這個操作成功(意味着許可的計數自從上一次讀取後就沒有被修改過)那麼將返回一個值表示獲取操作成功。在返回值中還包含了表示其他共享獲取操作能否成功的信息,如果成功,那麼其他等待的線程同樣會解除阻塞。

CAS(compareAndSet)算法的過程是這樣:它包含3個參數CAS(V,E,N)。V表示要更新的變量(內存值),E表示預期值(舊的),N表示新值。當且僅當V值等於E值時,纔會將V的值設爲N,如果V值和E值不同,則說明已經有其他線程做了更新,則當前線程什麼都不做。最後,CAS返回當前V的真實值。

//    14-16  Semaphore中的tryAuquireShared與tryReleaseShared
protected int tryAcquireShared(int acquires) {
   while (true) {
      int available = getState();
      int remaining = available - acquires; //首先計算剩餘許可的數量
      //如果還有剩餘的許可,那麼tryAcquireShared會通過compareAndSetState以原子方式來降低許可的計數。
      if (remaining < 0
             || compareAndSetState(available, remaining))
      //當沒有足夠的許可,或者當tryAuquireShared可以通過原子方式來更新許可的計數以響應獲取操作時,while循環將終止。
          return remaining;
   }
}
protected boolean tryReleaseShared(int releases) {
   while (true) {
      int p = getState();
      if (compareAndSetState(p, p + releases))
          return true;
   }
}

當沒有足夠的許可,或者當tryAuquireShared可以通過原子方式來更新許可的計數以響應獲取操作時,while循環將終止。
tryReleaseShared將增加許可計數,這可能會解除等待中線程阻塞狀態,並且不斷嘗試直到更新操作成功。
tryReleaseShared的返回值表示在這次釋放操作中解除了其他線程的阻塞。

CountDownLatch使用AQS的方式與Semaphore很相似:在同步狀態中保存的是當前的計數值。countDown方法調用release,從而導致計數值遞減,並且當計數值爲0時,解除所有等待線程的阻塞,await調用acquire方法,當計數器爲0時,acquire立即返回,否則將阻塞。

14.6.3 FutureTask

Future.get的語義非常類似與閉鎖的語義——如果發生了某個事件(由於FutureTask表示的任務執行完成或被取消),那麼線程就可以恢復執行,否則這些線程將停留在隊列中直到事件發生。

FutureTask中AQS的同步狀態被用來保存任務的狀態。例如,正在運行,已完成或已取消。FutureTask還維護一些額外的狀態變量,用來保存計算或拋出的異常。
此外,它還維護了一個引用,指向正在執行任務的線程(如果它當前處於運行狀態),因而如果任務取消,線程就會中斷

14.6.4 ReentrantReadWriteLock

ReadWriteLock接口表示存在兩個鎖:一個讀取鎖和一個寫入鎖,但在基於AQS實現的ReentrantReadWriteLock中,單個AQS子類將同時管理讀取加鎖和寫入加鎖。

ReentrantReadWriteLock分別使用了兩個16位的狀態來表示寫入鎖的計數和讀取鎖的計數。
在讀取鎖啥好過你的操作將使用共享的獲取方法和釋放方法,在寫入鎖上的操作將使用獨佔的獲取方法與釋放方法。

AQS在內部維護一個等待線程隊列,其中記錄了某個線程請求的是獨佔訪問還是共享訪問。
在ReentrantReadWriteLock中,當鎖可用時,如果位於隊列頭部的線程執行寫入操作,那麼線程會獲得這個鎖,
如果位於隊列頭部的線程執行讀取訪問,那麼隊列中在第一個寫入線程之前的所有線程都將獲得這個鎖。


小結

要實現一個依賴狀態的類——如果沒有滿足依賴狀態的前提條件,那麼這個類的方法必須阻塞,那麼最好的方式是基於現有的庫類來構建。

有時候現有的類庫不能提供足夠的功能,可以使用內置的條件隊列,顯式的Condition對象或AQS來構建自己的同步器。

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