?
接上一篇:?
http://caoyaojun1988-163-com.iteye.com/blog/1290759
?
4、運用:
AbstractQueuedSynchronizer類將上述功能聯系在一起,作為一個“模板方法模式[6]”中的模板類,作為其他同步器的基類。子類只是實現預定義方法,實現通過獲取鎖和釋放鎖的操作來檢查和更新狀態。然而,AbstractQueuedSynchronizer的子類本身不可用于ADTS,因為這些類暴露的用于內部控制獲取和釋放的策略的方法,應該對用戶不可見。所有java.util.concurrent包中的同步類聲明了一個私有的內部AbstractQueuedSynchronizer子類用于委托它的所有同步方法。這也使得public方法可以根據不太的同步器給予適當的名稱。
?
例如,假設最小的Mutex類,當同步狀態為0意味著解鎖,為1意味著鎖定。這個類的同步方法不需要參數值,因此直接默認使用零。
?
?
class Mutex { class Sync extends AbstractQueuedSynchronizer { public boolean tryAcquire(int ignore) { return compareAndSetState(0, 1); } public boolean tryRelease(int ignore) { setState(0); return true; } } private final Sync sync = new Sync(); public void lock() { sync.acquire(0); } public void unlock() { sync.release(0); } }?
?
此示例的更全面的版本,以及與其他使用文檔,可以在J2SE文檔中找到,當然還有許多變種。例如,tryAcquire可以使用“test-and-test-and-set“改變值之前檢查狀態值。
?
這可能是令人驚訝的,使用委托(delegation )和虛方法(virtual methods),來構造性能敏感的互斥鎖, 然而,各種面向對象的設計,動態編譯器早已非常成熟;當同步器頻繁調用的是。他們都可以很好的優化掉這個開銷;AbstractQueuedSynchronizer類還提供了一些方法,可以設置同步類的控制策略。例如,基于acquire方法的超時和中斷的版本、獨占模式的同步器、鎖。AbstractQueuedSynchronizer類也提供了一系列的方法(如acquireShared),他們與tryAcquireShared和tryReleaseShared方法不同,可以通知框架(通過其返回值)將來的acquires方法是否可以成功;最終可以實現級聯信號喚醒多個線程。
?
盡管序列化(永久存儲或傳輸)同步器通常是不明智的,但是由于這些類通常用于構建其他類,如線程安全的集合。所以通常實現序列化。AbstractQueuedSynchronizer和ConditionObject類提供的方法來序列化同步狀態,而不是被阻塞線程的底層或其他本質上臨時狀態。即便如此,大多數的同步類反序列化時只是復位同步狀態作為初始值,與內置鎖反序列化時總是設置為解鎖狀態的隱含的政策相同。雖然不是必須的,但仍明確支持final域的反序列化。
?
4.1 公平性
?
雖然他們是基于FIFO隊列,同步器不一定是公平的??梢宰⒁獾?,在基本的acquire算法中(3.3節),tryAcquire是再排隊前進行檢查。因此,新請求獲取鎖(acquiring)的線程可以優先于隊列的頭部第一個節點對應的線程。
?
這雖然破壞了FIFO的策略,但是也有普遍高于其他技術的總吞吐量。這減少了有鎖可以用,但是因為預定的下一個線程還在喚醒(unblocking)的過程中,所以還沒有線程獲取到鎖的時間,同時,它通過只允許一個(隊列的第一個)線程喚醒,避免了過度的、無用的、競爭;可以實現自己的tryAcquire方法,在交回控制權前簡單的多試幾次,這樣可以加劇不公平性, 如果有需要,開發者可以自己創建一個自己的簡單持有的同步器;
?
FIFO同步器是相對最公平的鎖;即便它會被打破,一個等待喚醒的(unpark)的線程與所有打破規則進入的線程都有一個公平的競爭機會,如果失敗它會重新阻塞;當然,如果闖入的線程比獲取一個等待喚醒的線程喚醒到達的快,隊列中第一個線程幾乎沒有贏的概率,所以幾乎總是reblock, 簡單持有的同步器,通常用于多個闖入線程和多個等待喚醒的線程在多處理器的場景,此時隊列中的第一個線程被喚醒;如下圖所示,既要維持一個或多個線程時處理器的利用率,同時也要避免饑餓。
?
如果需要更加公平的策略,也相對比較簡單。程序員可以自己定義 tryAcquire,如果不是隊列的head節點就失敗(返回false),達到嚴格的公平;可以通過少數提供的檢查方法之一getFirstQueuedThread,來檢查是否是第一個節點。
?
一個更快,更嚴格的變種是如果隊列(暫時)是空,也讓tryAcquire成功,在這種情況下,如果多個線程遇到一個空隊列的情況下競爭,其中至少有一個及第一個得到執行權的線程不需要入隊。這一策略使得在所有java.util.concurrent中的同步器,支持“公平”的模式。
?
雖然在實踐中往往希望公平,但是即便設置公平也不會得到保證,因為Java語言規范并不提供調度保證。例如,即使有一個嚴格意義上的公平同步,如果他們從來不需要阻止等待對方,JVM可以純粹按照順序選擇運行線程,而實際上,在單處理器的環境中,這些線程在被上下文切換之前可以每次運行一個時間片。如果有一個線程持有一個互斥鎖,它必須暫時先得到時間片才能釋放鎖,不然就阻塞那些需要鎖的線程,這導致延長了同步器是可用的,但是不能被線程獲取到的時間;使用公平的同步器往往在多處理器有很好的表現,因為這樣有更好的并發,因此出現競爭的幾率更多。
?
即使他們在高競爭的場景下,性能不是很理想,但是公平鎖任然可以工作的很好,同時也保持編碼的簡潔。例如:當維護相對較長的代碼或者延長鎖間的時間間隔,在這種情況下,可以提示一定的性能,但是饑餓的風險更大。同步框架將最終的決定權交由用戶。
?
4.2 同步器
?
這里描述一個草圖關于怎樣使用這個框架定義java.util.concurrent中的同步器;ReentrantLock類使用同步狀態(遞歸)維護鎖的數量。在獲取鎖的時候,它會記錄當前線程的ID,并且遞歸檢查是否有異常線程獲取鎖導致的非法的狀態異常。該類也提供ConditionObject,暴露它的監測和檢查方法。同時支持“公平”的模式的選項,在內部通過實現兩個不同AbstractQueuedSynchronizer的子類(公平的實現不允許打破規則),并為每個ReentrantLock的實例選擇以個合適的實現。
?
ReentrantReadWriteLock類使用16位的同步狀態持有寫鎖定計數,其余16位持有讀鎖計數。WriteLock使用與ReentrantLock同樣的結構。ReadLock使用acquireShared的方法,以允許多個讀者。
?
Semaphore類(一個計數信號)使用同步狀態保持當前計數。它定義acquireShared方法遞減計數,如果為負數了就阻塞當前線程;同樣使用tryRelease遞增計數,如果是正數的時候,就喚醒線程。
?
CountDownLatch類使用同步狀態代表計數。當減到零的時候所有所有獲取鎖的操作全部成功返回。
?
FutureTask類使用同步狀態來代表future 的運行狀態(初始,運行,注銷,完成)。調用釋放(release)來設置或取消一個future ,等待線程處理完成,acquire返回喚醒線程。
?
SynchronousQueue類(一個CSP風格的切換)使用內部等待節點調整生產者和消費者;它采用了同步狀態來作為標識,當消費者改變狀態的時候允許生產者繼續處理,反之亦然。
?
java.util.concurrent包的用戶當然可以為自己的應用程序定義自己的同步器;例如,在那些經常遇到,但是包里沒有提供的各種Win32事件的語義類,二進制閂鎖,鎖集中管理,并基于樹的壁壘。
?
原文見第一篇的附件
下一篇:http://caoyaojun1988-163-com.iteye.com/blog/1315089