java併發編程實踐學習(14 ) 構建自定義的同步工具

##一.管理狀態依賴性

在單線程化的程序中,如果調用一個方法時,依賴於狀態的先驗條件爲滿足(比如連接池非空),那麼這個先驗條件就無法變爲真。但是在併發程序中,基於狀態的先驗條件會在其他線程的活動中被改變。對於併發對象,依賴於狀態的方法有時可以再不能滿足先驗條件的情況下選擇失敗,不過更好的選擇是等待先驗條件變爲真。
有一種依賴於狀態的操作,能夠被阻塞直到可以繼續執行。內部條件隊列的機制可以讓線程一直阻塞,指導對象已經進入某個特定的狀態,該狀態下進程可以繼續執行,並且在阻塞線程可以進行進一步執行的時候,對象會將它們喚醒。“輪訓與休眠”來解決狀態依賴操作(費力不討好)。

1.將先驗條件失敗傳給調用者

當先驗條件不滿足時,調用者可以不用休眠而是直接重試操作-這種被稱作忙等待或者自旋等待。如果在相當長的一段時間內,緩存的狀態都不會改變,那麼使用這種方法就會消耗相當多的CPU時間。另一個方面,調用者可以決定休眠,以避免消耗過多的CPU時間,但是如果緩存的狀態在休眠不久的時候很快發生了變化,那麼他很容易睡過頭每次迭代中,在等待與休眠之間一種折中的選擇是調用Thread.yield,這給調度器一個提示:我現在可以讓出一定的時間讓另外的線程運行。

2.利用“輪詢加休眠”實現拙劣的阻塞

輪訓和休眠重試機制爲每次調用實現了重試邏輯,從而分擔調用者的麻煩。如果緩存是空的take將休眠,直到另一個線程在緩存中置入了一些數據,
有限緩存使用了拙劣的阻塞

@ThreadSafe
public class SleepyBoundedBuffer<V> extends BaseBoundedBuffer<V>{
    public SleepyBoundedBuffer(int siz){
        super(size);
    }
    public void put(V v) throws InterruptedException{
        while(true){
            synchronized (this){
                if(!isFull){
                    doPut(v);
                    return;
                }
            }
        }
    }

    public V take() throws InterruptedException{
        while(true){
            sychronized (this){
                if(!isEmpty){
                    return doTake();
                }
            }
        }
    }
}

然而這樣講輪訓和休眠組合成一個阻塞操作的嘗試都不能滿意。

3.讓條件隊列來解決這一切

條件隊列可以讓一組線程—稱作工作集以某種方式等待相關條件變成真。不同於傳統隊列,它們的元素是數據項;條件隊列的元素是等待相關條件的線程。
有限緩存使用條件隊列

@ThreadSafe
public class BoundedBuffer<V> extends BaseBoundedBuffer<V>{
    //條件謂詞:not-full(!isFull)
    //條件謂詞:not-empty(!isEmpty)
    public BoundedBuffer(int size){
        super(size);    
    }

    public synchronized void put(V v)throws InterruptedException{
        while(isFull())
            wait();
        doPut(v);
        notifyAll();
    }

    public synchronized V take()throws InterruptedException{
        while(isEmpty)
            wait();
        V v = doTake();
        notifyAll();
        return v;
    }
}

BoundedBuffered目前已經足夠好了——簡單夠用,而且把狀態獨立性管理的非常清晰。

二.使用條件隊列

條件隊列讓構建有效且可響應的狀態依賴變得容易。但是將它們用錯也很容易。

1.條件謂詞

正確使用條件隊列的關鍵在於識別出對象可以等待的條件謂詞。條件謂詞是先驗條件的第一站,它在一個操作與狀態只建立起依賴關係。

將條件謂詞和與之關聯的條件隊列,以及在隊列中等待的操作,都寫入文檔。

2.過早的喚醒

一個單獨的內部條件隊列可以與多個條件謂詞共同使用。
當有人調用notifyAll,喚醒了你的線程的時候,並不意味着你正在等待的條件謂詞變成真了。所以當你從wait中喚醒後,都必須再次測試條件謂詞,如果條件謂詞尚未成真,就繼續等待或失敗。

當使用條件等待時:

  • 永遠設置一個條件謂詞——一些對象狀態的測試,線程執行前必須滿足它。
  • 永遠在調用wait前測試條件謂詞,並且從wait中返回後再次測試。
  • 永遠在循環中調用wait
  • 確保構成條件謂詞的狀態變量被鎖保護,而這個鎖正是與條件隊列相關聯的。
  • 當調用wait、notify或者notifyAll時,要持有與條件隊列相關量的鎖
  • 在檢查條件謂詞之後、開始執行被保護的邏輯之前不要釋放鎖。

3.丟失的信號

死鎖和活鎖可以導致活躍度失敗,另一種形式的活躍度失敗是丟失的信號。當一個線程等待的條件已經爲真,但是進入等待前的條件謂詞卻返回了假,我們稱這樣就出現了一個丟失的信號。

4.通知

無論何時當你在等待一個條件時,一定要確保有人會在條件謂詞變爲真時通知你。
在條件隊列中有兩個通知方法——notify和notifyAll。無論調用哪一個你都必須持有與條件隊列相關聯的鎖。調用notify的結果是:JVM會在這個條件隊列中等待的衆多線程中挑選出一個,並把它喚醒。而你調用notifyAll會喚醒所有正在這個條件隊列中等待的線程。

只有同時滿足下述條件後才能使用單一的notify取代notifyAll
相同的等待者。只有一個條件謂詞與條件隊列相關,每個線程,從wait返回後執行相同的邏輯
一進一出,一個對條件變量的通知,至多隻激活一個線程執行。

5.子類安全問題

一個依賴於狀態的類,要麼完全將它的等待和通知協議暴露(並文檔化給子類),要我們完全阻止子類參與其中。

6.封裝條件隊列

通常最好可以把條件隊列封裝起來,這樣在使用它的類層次結構之外,是不能訪問它的。

7.入口協議和出口協議

Wellings用“入口協議和出口協議”的形式刻畫了wait和notify的正確使用方法。對於每個依賴於狀態的操作,以及每個修改了其他狀態的操作,你都應該爲其定義並文檔化一個入口協議和出口協議。入口協議就是操作的條件謂詞,出口協議涉及到要檢查任何備操作改變的狀態變量,確認它們是否引起其他一些條件謂詞變爲真,如果是,通知相關的條件隊列。

三、顯示的Condition對象

Condition是廣義的內部條件隊列。內部條件隊列有一些缺陷,每個內部鎖只能有一個與之相關的條件隊列。這意味着多個線程可能爲了不同的條件謂詞在同一個條件隊列中等待,而且大多數常見的顯示鎖都會暴露條件隊列對象。

危險警告:wait、notify和notifyAll在Condition中的對等體是await、signal和signalAll.但是Condition繼承於Object所以它也有wait和notify,一定要使用正確-await和signal

四.剖析Synchronizer

Reentrant和Semaphore這倆個接口有很多共同點。這些類都扮演了“閥門”的角色,每次只允許優先數目的線程通過它;線程到達閥門後,可以通過(lock或acquire成功返回),可以等待(lock或acquire阻塞),也可以被取消(tryLock或tryAcquire返回false,指明在允許的時間內,鎖或者“許可”不可用), 更進一步它們都允許可中斷的,不可中斷的,可限時的請求嘗試,它們也都允許選擇公平、非公平的等待線程隊列。
事實上它們的實現都用到共同的基類,AbstractQueuedSynchronizer(AQS),它是一個用來構建鎖和Synchronizer的框架

五、AbstractQueuedSynchronized

一個基於AQS的Synchronizer所執行的基本操作,是一些不同形式的獲取和釋放。獲取操作是狀態依賴的操作,總能夠阻塞。藉助鎖和信號量。“獲取”的含義變得相當直觀—獲取鎖或者許可—並且調用者可能不得不去等待,直到Synchronizer處於可發生的狀態。CountDownLatch的請求意味着“等待,直到閉鎖到達它的終止狀態“FutureTask意味着“等待,直到任務已經完成”。“釋放”不是一個可阻塞的操作;“釋放”可以允許線程在請求執行前阻塞。
支持獨佔獲取的Synchronizer應該實現tryAcquire、tryRelease和isHeldExclusively這幾個受保護的方法,而支持共享獲取的Synchronizer應該實現tryAcquireShared。

1.一個簡單的閉鎖

二元閉鎖使用

@ThreadSafe
public class OneShotLatch{
    private final Sync sync = new Sync();
    public void signal(){
        sync.releaseShared(0);
    }

    public void await()throws InterruptedException{
        sync.acquireSharedInterruptibly(0);
    }

    private class Sync extends AbstractQueueSynchronizer{
        //如果閉鎖打開成功(state==1)否則失敗
        return (getState()==1?1:-1;
    }

    protected boolean tryReleaseShared(int ignored){
        setState(1);//閉鎖已經打開
        return true;//現在其他線程可以獲得閉鎖
    }
}

6、java.util.concurrent的Synchronizer類中的AQS

1.ReentrantLock

ReentrantLock只支持獨佔的獲取操作,因此他實現了tryAcquire、tryRelease和isHeldExclusively。ReentrantLock使用同步狀態持有鎖獲取操作的計算,還維護一個owner變量來持有的線程標識符。
當一個線程嘗試去獲取鎖,tryAcquire會首先請求鎖的狀態。如果鎖未被佔用,它會嘗試更新鎖的狀態,表明鎖已被佔有。它還利用AQS內置的對多條件變量和多等待集的支持。###2.Semaphore和CountDownLatch

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