Java併發程式設計實戰--筆記四

衣舞晨風發表於2017-10-13

第13章 顯式鎖

     為什麼要建立一種與內建鎖如此現實的加鎖機制?在大多數情況下,內建鎖能很好地工作,但在功能上存在一些侷限性,例如,無法中斷一個正在等待獲取鎖的執行緒,或者無法再請求一個鎖時無限地等待下去。內建鎖必須在獲取該鎖的程式碼塊中釋放,這就簡化了編碼工作,並且與異常處理操作實現了很好地互動,但卻無法實現非阻塞結構的加鎖規則。這些都是使用synchronized的原因,但在某些情況下,一種更靈活的加鎖機制通常能提供更好地活躍性和效能,因為它可以跨方法級來釋放。
     在一些內建鎖無法滿足需求的情況下,ReentrantLock可以作為一種高階工具。當需要一些高階功能時才應該使用ReentrantLock,這些功能包括:可定時的、可輪詢的與可中斷的鎖獲取操作,公平佇列,以及非塊結構的鎖。否則還是應該優先使用synchronized。

     僅當內建鎖不能滿足需求時,才可以考慮使用ReentrantLock。

     讀 - 寫鎖是一種效能優化措施,在一些特定的情況下能實現更高的併發性。在實際情況中,對於在多處理器系統上被頻繁讀取的資料結構,讀 - 寫鎖能夠提高效能。而在其他情況下,讀 - 寫鎖的效能比獨佔鎖的效能要略差一些,這是因為它們的複雜性更高。如果要判斷在某種情況下使用讀 - 寫鎖是否會帶來效能提升,最好對程式進行分析。由於ReadWriteLock使用Lock來實現鎖的讀 - 寫部分,因此如果分析結果表明讀 - 寫鎖沒有提高效能,那麼可以很容易地將讀 - 寫鎖換為獨佔鎖。

     當鎖的持有時間較長並且大部分操作都不會修改被守護的資源時,那麼讀 - 寫鎖能提高併發性。現實中,ConcurrentHashMap的效能已經足夠好了,所以你可以使用它,而不必使用這個新的解決方案,但如果需要對另一種Map實現(例如LinkedHashMap)提供併發性更高的訪問,那麼這技術是非常有用的。

     與內建鎖相比,顯式的Lock提供了一些擴充套件功能,在處理鎖的不可用性的方面有著更高的靈活性,並且對佇列有著更高的控制。但ReentrantLock不能完全替代synchronized,只有在synchronized無法滿足需求時,才應該使用它。

     讀 - 寫鎖允許多個讀執行緒併發地訪問被保護的物件,當訪問以讀取操作為主的資料結構時,它能提高程式的可伸縮性。

第14章 構建自定義的同步工具

     每個Java物件都可以作為一個鎖,每個物件同樣可以作為一個條件佇列,並且Object中的wait,notify和notifyAll方法構成了內部條件佇列的API.物件的內建鎖與其內部條件佇列是相關聯的,要呼叫物件X中條件佇列的任何一個方法,必須持有物件X的鎖.

     這是因為”等待由狀態構成的條件”與”維護狀態一致性”這兩種機制必須被緊密地繫結一起,只有能對狀態進行檢查時,才能在某個條件上等待,並且只有能修改狀態時,才能從條件等待中釋放另一個執行緒.

     Object.wait會自動釋放鎖,並請求作業系統掛起當前執行緒,從而使其他執行緒能夠獲得這個鎖並修改物件的狀態。當被掛起的執行緒醒來時,它將在返回之前重新獲得鎖。

     從直觀上來理解,呼叫wait意味著“我要去休息了,但當發生特定的事情時喚醒我”,而呼叫通知方法就意味著“特定的事情發生了”。

     Object.notify和Object.notifyAll能喚醒正在等待執行緒,從條件佇列中選取一個執行緒喚醒並嘗試重新獲取鎖。

     條件謂詞是使某個操作成為狀態依賴的前提條件。在有界快取中,只有當快取不為空時,take方法才能執行,否則必須等待。對take方法來說,它的條件謂詞就是“快取不為空”,take方法在執行之前必須首先測試該條件謂詞。同樣,put方法的條件謂詞是“快取不滿”。條件謂詞是由類中各個狀態變數構成的表示式。

     在條件等待中存在一種重要的三元關係,包括加鎖、wait方法和一個條件謂詞。在條件謂詞中包含多個狀態變數,而狀態變數由一個鎖來保護,因此在測試條件謂詞之前必須先持有這個鎖。鎖物件與條件佇列物件(即呼叫wait和notify等方法所在的物件)必須是同一個物件。

     當執行緒從wait方法中被喚醒時,它在重新請求鎖時不具有任何特殊的優先順序,而要與任何其他嘗試進入同步程式碼塊的執行緒一起正常地在鎖上進行競爭。

     每一次wait呼叫時都會隱式地與特定的條件謂詞關聯起來。當呼叫某個特定條件謂詞的wait時,呼叫者必須已經持有與條件佇列相關的鎖,並且這個鎖必須保持著構成條件謂詞的狀態變數。

     每當在等待一個條件時,一定要確保在條件謂詞變為真時通過某種方法發出通知。

     在呼叫notify時,JVM會從這個條件佇列上等待的多個執行緒中選擇一個來喚醒,而呼叫notifyAll則會喚醒所有在這個條件佇列上等待的執行緒。由於在呼叫notifyAll或notify時必須持有條件佇列物件的鎖,而如果這些等待中執行緒此時不能重新獲得鎖,那麼無法從wait返回,因此發出通知的執行緒應該儘快地釋放鎖,從而確保正在等待的執行緒儘可能快地解除阻塞。

     只要同時滿足以下兩個條件時,才能使用單一的notify而不是notifyAll:

     ● 所有等待執行緒的型別都相同 只有一個條件謂詞與條件佇列相關,並且每個執行緒在從wait返回後將執行相同的操作

     ● 單進單出 在條件變數上的每次通知,最多隻能喚醒一個執行緒來執行。

     當只有一個執行緒可以執行時,如果使用notifyAll,那麼將是低效的,這種低效情況帶來的影響有時候很小,但有時候卻非常大。如果有10個執行緒在一個條件佇列上等待,那麼呼叫notifyAll將喚醒每一個執行緒,並使得它們在鎖上發生競爭。然後,它們中的大多數或者全部又都回到休眠狀態。因而,在每個執行緒執行一個事件的同時,將出現大量的上下文切換操作以及發生競爭的鎖獲取操作。(最壞的情況是,在使用notifyAll時將導致O(n2)次喚醒操作,而實際上只需要n次喚醒操作就足夠了)。這是“效能考慮因素與安全性因素相互矛盾”的另一種情況。

     要麼圍繞著繼承來設計和文件化,要麼禁止使用繼承。

     對於每個依賴狀態的操作,以及每個修改其他操作依賴狀態的操作,都應該定義一個入口協議和出口協議。入口協議就是該操作的條件謂詞,出口協議則包括,檢查被該操作修改的所有狀態變數,並確認它們是否使某個其他的條件謂詞變為真,如果是,則通知相關的條件佇列。

     在ReentrantLock和Semaphore這兩個介面之間存在許多共同點。這兩個類都可以用作一個”閥門“,即每次只允許一定數量的執行緒通過,並當執行緒到達閥門時,可以通過(在呼叫lock或acquire時成功返回),也可以等待(在呼叫lock或acquire時阻塞),還可以取消(在呼叫tryLock或tryAcquire時返回”假“,表示在指定的時間內鎖是不可用的或者無法獲取許可)。而且,這兩個介面都支援中斷、不可中斷的以及限時的獲取操作,並且也都支援等待執行緒執行公平或非公平的佇列操作。

     在ReentrantLock和Semaphore這兩個介面之間存在許多共同點。這兩個類都可以用作一個”閥門“,即每次只允許一定數量的執行緒通過,並當執行緒到達閥門時,可以通過(在呼叫lock或acquire時成功返回),也可以等待(在呼叫lock或acquire時阻塞),還可以取消(在呼叫tryLock或tryAcquire時返回”假“,表示在指定的時間內鎖是不可用的或者無法獲取許可)。而且,這兩個介面都支援中斷、不可中斷的以及限時的獲取操作,並且也都支援等待執行緒執行公平或非公平的佇列操作。

第15章 原子變數與非阻塞同步機制

     從Java5.0開始,可以使用原子變數類(例如AutomicInteger和AutomicReference)來構建高效地非阻塞演算法

     即使原子變數沒有用於非阻塞演算法的開發,它們也可以用做一種“更好的volatile型別變數”。原子變數提供了與volatile型別變數相同的記憶體語義,此外還支援原子的更新操作,從而使它們更加適用於實現計數器、序列發生器和統計資料收集等,同時還能比基於鎖的方法提供更高的可伸縮性。

     volatile變數是一種更輕量級的同步機制,因為在使用這些變數時不會發生上下文切換或執行緒排程等操作。然而,volatile變數同樣存在一些限制:雖然它們提供了相似的可見性保證,但不能用於構建原子的複合操作。因此,當一個變數依賴其他的變數時,或者當變數的新值依賴於舊值時,就不能使用volatile變數。

     在大多數處理器架構(包括IA32和Sparc)中採用的方法是實現一個比較並交換(CAS)指令。(在其他處理器中,例如PowerPC,採用一對指令來實現相同的功能:關聯載入與條件儲存。)CAS包含了3個運算元——需要讀寫的記憶體位置V、進行比較的值A和擬寫入的新值B。當且僅當V的值等於A時,CAS才會通過原子方式用新值B來更新V的值,否則不會執行任何操作。無論位置V的值是否等於A,都將返回V原有的值。(這種變化形式被稱為比較並設定,無論操作是否成功都會返回。)CAS的含義是:“我認為V的值應該為A,如果是,那麼將V的值更新為B,否則不修改並告訴V的值實際為多少”。CAS是一項樂觀的技術,它希望能成功地執行更新操作,並且如果有另一個執行緒在最近一次檢查後更新了該變數,那麼CAS能檢測到這個錯誤。

     下面程式說明了CAS語義(而不是實現或效能):

@ThreadSafe
public class SimulatedCAS {//模擬CAS操作
    @GuardedBy("this") private int value;
    public synchronized int get() { return value; }
    public synchronized int compareAndSwap(int expectedValue,
                                           int newValue) {//相當於處理器的CAS原子化操作
        int oldValue = value;
        if (oldValue == expectedValue)
            value = newValue;
        return oldValue;
    }
    public synchronized boolean compareAndSet(int expectedValue,
                                              int newValue) {
        return (expectedValue
                == compareAndSwap(expectedValue, newValue));
    }
}

     當多個執行緒嘗試使用CAS同時更新同一個變數時,只有其中一個執行緒能更新變數的值,而其他執行緒都將失敗。然而,失敗的執行緒並不會被掛起(這與獲取鎖的情況不同:當獲取鎖失敗時,執行緒將被掛起),而是被告知在這次競爭中失敗,並且可以再次嘗試。由於一個執行緒在競爭CAS時失敗時不會阻塞,因此它可以決定是否重新嘗試,或者執行一些恢復操作,也或者不執行任何操作。這種靈活性就大大減少了與鎖相關的活躍性風險(儘管在一些不常見的情況下仍然存在活鎖風險)。

     CAS的典型使用模式是:首先從V中讀取值A,並根據A計算新值B,然後再通過CAS以原子方式將V中的值由A變成B(只要在這期間沒有任何執行緒將V的值修改為其他值)。由於CAS能檢測到來自其他執行緒的干擾,因此即使不使用鎖也能夠實現原子的讀-改-寫操作序列。

     CAS的效能會隨著處理器數量的不同而變化很大。在單CPU系統中,CAS通常只需要很少的時鐘週期,因為不需要處理器之間的同步。非競爭的CAS在多CPU系統中至少需要10至150個時鐘週期的開銷。CAS的執行效能不僅在不同體系架構之間變化很大,甚至在相同處理器的不同版本之間也會發生改變。一個很管用的經驗法則是:在大多數處理器上,在無競爭的鎖獲取和鎖釋放的“快速程式碼路徑”上的開銷,大約是CAS開銷的兩倍。

     Java程式碼如何確保處理器執行CAS操作?在Java5.0之前,如果不編寫明確的程式碼,那麼就無法執行CAS。在Java5.0中引入了底層的支援,在int、long和物件的引用等型別上都公開了CAS操作,並且JVM把它們編譯為底層硬體提供的最有效方法。在支援CAS的平臺上,執行時把它們編譯為相應的(多條)機器指令。在最壞的情況下,如果不支援CAS指令,那麼JVM將使用自旋鎖。在原子變數類(例如java.util.concurrent.atomic中的AutomicXxx)中使用了這些底層的JVM支援為數字型別和引用型別提供了一種高效的CAS操作,而在java.util.concurrent中的大多數類在實現時則直接或間接地使用了這些原子變數類。

     原子變數相當於一種泛化的volatile變數,能夠支援原子的和有條件的讀-改-寫操作。AtomicInteger表示一個int型別的值,並提供了get和set方法,這些Volatile型別的int變數在讀取和寫入上有著相同的記憶體語義。它還提供了一個原子的compareAndSet方法(如果該方法執行成功,那麼將實現與讀取 / 寫入一個volatile變數相同的記憶體效果),以及原子的新增、遞增和遞減等方法。

     共有12個原子變數類,可分為4組:標量類(Scalar)、更新器類、陣列類以及複合變數類。最常用的原子變數就是標量類:AtomicInteger、AutomicLong、AutomicBoolean以及AtomicReference。所有這些都支援CAS,此外,AtomicInteger和AtomicLong還支援算術運算。

     鎖與原子變數在不同競爭程度上的效能差異很好地說明了各自的優勢和劣勢。在中低程度的競爭下,原子變數能提供更好的可伸縮性,而在高強度的競爭下,鎖能夠更有效地避免競爭。

     如果在某種演算法中,一個執行緒的失敗或掛起不會導致其他執行緒也失敗或掛起,那麼這種演算法就被稱為非阻塞演算法。如果在演算法的每個步驟中都存在某個執行緒能夠執行下去,那麼這種演算法也被稱為無鎖(Lock-Free)演算法。

     建立非阻塞演算法的關鍵在於,找出如何將原子修改的範圍縮小到單個變數上,同時還要維護資料的一致性。

第16章 java記憶體模型

     Happens-Before的規則包括:

     程式順序規則。如果程式中操作A在操作B之前,那麼線上程中A操作將在B操作之前執行。

     監視器鎖規則。在監視器鎖上的解鎖操作必須在同一個監視器鎖上的加鎖操作之前執行。(顯式鎖和內建鎖在加鎖和解鎖等操作上有著相同的記憶體語義)

     volatile變數規則。對volatile變數的寫入操作必須在對該變數的讀操作之前執行。(原子變數與volatile變數在讀操作和寫操作上有著相同的語義)

     執行緒啟動規則。線上程上對Thread.start的呼叫必須在該執行緒中執行任何操作之前執行。

     執行緒結束規則。執行緒中的任何操作都必須在其他執行緒檢測到該執行緒已經結束之前執行,或者從Thread.join中成功返回,或者在呼叫Thread.isAlive時返回false。

     中斷規則。當一個執行緒在另一個執行緒上呼叫interrupt時,必須在被中斷執行緒檢測到interrupt呼叫之前執行(通過丟擲InterruptException,或者呼叫isInterrupted和interrupted)。

     終結器規則。物件的建構函式必須在啟動該物件的終結器之前執行完成。

     傳遞性。如果操作A在操作B之前執行,並且操作B在操作C之前執行,那麼操作A必須在操作C之前執行。

     靜態初始化是JVM完成的,發生在類的初始化階段(載入、連結、類的初始化),即類被載入後到類被任意執行緒使用之前。JVM會在初始化期間獲得一個鎖,這個鎖每個執行緒都至少會獲取一次,來確保類是否已被載入;這個鎖也保證了靜態初始化期間,記憶體寫入的結果自動地對照所有執行緒都是可見的。所以靜態初始化的物件,無論是構造期間還是被引用的時候,都不需要顯試地進行同步。然而,這僅僅適用於構造當時的狀態——如果物件是可變的,為了保證後續修改的可見性,仍然需要同步。

     初始化安全性將確保,對於被正確構造的物件,所有執行緒都能看到由建構函式為物件給各個final域設定的正確值,而不管採用何種方式來發布物件。而且對於可以通過被正確構造物件中某個final域到達的任意變數(例如某個final陣列中的元素,或者由一個final域引用的HashMap的內容)將同樣對於其他執行緒是可見的。(這僅僅適用於那些在構造過程中從物件的final域出發可以到達的物件)

     初始化安全性只能保證通過final域可達的值從構造過程完成時可見性。對於通過非final域可達的值,或者在構成過程完成後可能改變的值,必須採用同步來確保可見性。

Java併發程式設計實戰pdf及案例原始碼下載:
http://download.csdn.net/detail/xunzaosiyecao/9851028

個人微信公眾號:
這裡寫圖片描述

作者:jiankunking 出處:http://blog.csdn.net/jiankunking

相關文章