第六章 保障線程安全的設計技術--《java多線程編程實戰指南-核心篇》

棧空間是爲線程的執行而準備的一段固定大小的內存空間,每個線程都由其棧空間。棧空間實在線程創建的時候分配的。線程執行一個方法前,java虛擬機會在該線程的棧空間中爲這個方法調用創建一個棧幀。棧幀用於存儲相應方法的局部變量、返回值等私有數據。

無狀態對象不含任何實例變量,不包含任何靜態變量或者其包含的靜態變量都是隻讀的,是線程安全的。

不可變對象是指一經創建其狀態就保持不變的對象,也具有線程安全性。

不可變對象需滿足以下條件:

  • 類本身使用final修飾,這是爲了防止通過創建子類來改變其定義的行爲。
  • 所有字段都是用final修飾的:使用final修飾不僅僅是從語義上說明被修飾字段的值不可改變;更重要的是這個語義在多線程環境下保證了被修飾字段的初始化安全,即final修飾的字段在對其他線程可見時,它必定是初始化完成的。
  • 對象在初始化過程中沒有逸出:防止其他類(如該類的內部匿名類)在對象初始化過程中修改其狀態。
  • 任何字段,若其引用了其他狀態可變的對象(如集合、數組等),則這些字段必須是private修飾的,並且這些字段值不能對外暴露。若有相關方法要返回這些字段值,則應該進行防禦性賦值如調用Collections.unmodifiableSet()。

ThreadLocal

ThreadLocal是線程持有對象,該實例通常會被作爲某個類的靜態字段使用。

ThreadLocal可能存在的問題:1.退化和數據錯亂,即當一個線程執行多個任務時,殘留上個任務執行的結果,因此在每次使用前應當先清空;2.ThreadLocal可能導致內存泄漏、僞內存泄漏。

ThreadLocal的內部實現機制:在java平臺中,每個線程(Thread實例)內部會維護一個類似HashMap的對象,我們稱之爲ThreadLocalMap。每個ThreadLocalMap內部都會包含若干個Entry。因此,我們可以說每個線程都擁有若干個這樣的條目,相應的線程就被稱爲這些條目的屬主線程。Entry的key是一個ThreadLocal實例,value是一個線程持有對象。因此,Entry的作用相當於爲其屬主線程建立起一個ThreadLocal實例與一個線程特有對象之間的對應關係。由於Entry對ThreadLocal實例的引用(通過key引用)是一個弱引用,因此它不會阻止被引用的ThreadLocal實例被垃圾回收,即其所在的Entry的Key會被置爲null。此時,相應的Entry就成了無效條目。另一方面,由於Entry對線程特有對象的引用是強引用,因此如果無效條目本身對他的可達強引用,那麼無效條目也會阻止其引用的線程特有對象被垃圾回收。有鑑於此,當ThreadLocalMap中有新的ThreadLocal到線程持有的對象的映射關係被創建(相當於有新的Entry被添加到ThreadLocalMap)的時候,ThreadLocalMap會將無效條目清理掉,這打破了無效條目對象特有對象的強引用,從而使相應的線程特有對象能夠被垃圾回收。但是,這個處理也有一個缺點--一個線程訪問過線程局部變量之後如果改線程有對其可達的強引用,並且該線程長時間內處於非運行狀態,那麼該線程的ThreadLocalMap可能就不會有任何變化,因此相應的ThreadLocalMap中的無效條目也不會被清理,這就可能導致這些線程的各個Entry所引用的線程特有對象都無法被垃圾回收,即導致了僞內存泄漏。

內存泄漏指由於對象無法永遠無法被垃圾回收導致其佔用的java虛擬機內存無法被釋放。持續的內存泄漏會導致java虛擬機可用的內存主鍵減少,並最終可能導致java虛擬機內存溢出OOM,知道JVM宕機。

僞內存泄漏類似於內存泄漏。所不同的是,僞內存泄漏中對象所佔用的內存在其不再被使用後的相當長時間仍然無法被回收,甚至可能永遠無法被回收。業績就是說,僞內存泄漏中對象佔用的內存空間可能會被回收,也可能永遠無法被回收。

裝飾器模式

裝飾器模式可以用來實現線程安全,其基本思想是爲非線程安全對象創建一個相應的線程安全的外包裝對象,客戶端代碼不直接訪問非線程安全對象而是訪問其外包裝對象。外包裝對象與相應的非線程安全對象具有相同的接口,因此客戶端代碼使用外包裝對象的方式與直接使用相應的非線程安全對象的方式相同,而外包裝對象內部通常會藉助鎖,以線程安全的方式調用響應非線程安全對象的同簽名方法來實現其對外暴露的各個方法。如:

Collections.synchronizedList()、Collections.synchronizedMap()、Collections.synchronizedXXX()。。。。

使用裝飾器模式來實現線程安全的一個好處就是關注點分離;

使用裝飾器模式實現線程安全存在一些缺點(特指上方jdk提供的方式):

  • 首先這些同步集合的iterator方法返回的Iterator實例並不是線程安全的。爲了保障對同步集合的遍歷操作的線程安全性,我們需要對遍歷操作進行加鎖
    package JavaCoreThreadPatten.capter06;
    
    import java.util.ArrayList;
    import java.util.Collections;
    import java.util.Iterator;
    import java.util.List;
    
    public class SyncCollectionSafeTraversal {
        final List<String> syncList = Collections.synchronizedList(new ArrayList<>());
        public void test(){
            Iterator<String> iterator = syncList.iterator();
            //需要對該對象進行加鎖,因爲返回的Iterator是非線程安全的,降低了併發性能
            synchronized (syncList){
                while (iterator.hasNext()){
                    System.err.println(iterator.next());
                }
            }
        }
    }
    

    對同步集合進行遍歷操作的時候,我們需要以被遍歷同步集合對象本身作爲內部鎖。這樣做實質上是利用了內部鎖的排他性,從而阻止了遍歷過程中其他線程改變了同步集合的內部結構。因此,這種遍歷是不利於提高併發性的。另外,對便利操作進行加鎖時,我們選用的內部鎖必須和相應的同步集合內部用於保障其自身線程安全所使用的鎖保持一致。也就是說,這一定程度上要求我們必須知道同步集合對象內部的一些細節,顯然這是有悖於面向對象編程中的信息封裝原則。

  • 其次,這些同步集合在其實現線程安全的時候通常是使用一個粗粒度的鎖,即使用一個鎖來保護其內部所有的共享狀態。因此,這些同步集合雖然可以確保線程安全,但是也可能導致鎖的高爭用,從而導致較大的上下文切換的開銷。

併發集合

併發集合對象自身就支持對其進行線程安全的遍歷操作。應用代碼對併發集合對象進行遍歷的時候無需加鎖就可以實現便利操作的線程安全。並且,對併發集合的遍歷操作和對其進行更新操作是可以由不同的線程併發執行的,從而有利於充分提高系統的併發性。

併發集合實現線程安全的遍歷通常由兩種方式。一種是對待遍歷對象的快照進行遍歷。快照是在Iterator實例被創建的那一刻待遍歷對象內部結構的一個只讀副本,它反映了待遍歷集合的某一時刻的狀態。由於對同一個併發集合進行遍歷操作的每個線程會得到各自的一分快照,因此快照相當於這些線程的線程特有對象。所以,這種方式下進行遍歷操作的線程無需加鎖就可以實現線程安全。另外,由於快照是隻讀的,因此這種遍歷方式所返回的Iterator實例是不支持remove方法的。這種方式的優點是遍歷操作和更新操作之間互不影響,缺點是當被遍歷的集合比較大時,創建快照的直接或者間接開銷會比較大。CopyOnWriteArrayList和CopyOnWriteArraySet就使用這種遍歷方式。另一種是對待遍歷對象進行準實時的變量。所謂準實時是指遍歷操作不是針對待遍歷對象的副本進行的,但又不借助鎖來保障線程安全,從而使得遍歷操作可以與更新操作併發進行。並且,遍歷過程中其他線程對被遍歷對象的內部結構的更新可能會(也可能不會)被反映出來。這種遍歷方式所返回的Iterator實例可以支持remove方法。ConcurrentLinkedQueue和ConcurrentHashMap等併發集合就採用這種遍歷方式。由於Iterator是被設計用來一次只被一個線程使用的,因此如果有多個線程需要進行遍歷操作,那麼這些線程之間是不適宜共享同一個Iterator實例的。

如果有多個線程需要對同一個併發集合進行遍歷操作,那麼這些線程不適合共享同一個Iterator實例

另外,併發集合內部在保障其線程安全的時候通常不借助鎖,而是使用CAS操作,或者對鎖進行了優化,比如使用粒度極小的鎖。因此,併發集合的可伸縮性一般要比相應的同步集合高,即使用併發集合的程序相比於使用相應同步集合的程序而言,併發線程數的增加所帶來的程序的吞吐率的提升要更加顯著。而使用同步集合的程序隨着併發線程數量的上升,這些同步集合內部所使用的鎖的爭用所導致的上下文切換開銷越來越大,最終有可能使程序的吞吐率一定程度上降低或者恆定到一定的水平。

ConcurrentLinkedQueue是Queue接口的一個線程安全類,它相當於LinkedList的線程安全辦,可以作爲Collections.synchronizedList()的替代品。ConcurrentLinkedQueue內部訪問其共享狀態變量的時候並不需要藉助鎖,而是使用CAS操作來保障線程安全的。因此,ConcurrentLinkedQueue是非阻塞的,其使用不會導致當前線程被暫停,因此也就避免了上下文切換的開銷。ConcurrentLinkedQueue所使用的遍歷方式是準實時。與BlockQueue的實現類相比,ConcurrentLinkedQueue更適合於更新操作和遍歷操作併發的場景,比如一個或多個線程往/從隊列中添加/刪除元素,而另一個或多個線程則對相應隊列進行遍歷操作。而BlockingQueu的實現類則更適合於多個線程併發更新同一隊列的場景,比如在生產者-消費者模式中生產者線程往隊列中添加元素,而消費者線程從隊列中移除元素。(這個對比沒看明白

ConcurrentHashMap是Map接口的一個線程安全實現類,它相當於HashMap的線程安全版,可以作爲Hashtable和Collections.synchronizedMap()的替代品。ConcurrentHashMap內部使用類粒度極小的鎖(分段鎖)來保障其線程安全。ConcurrentHashMap的讀取操作基本上不會導致鎖的使用。另外,默認情況下ConcurrentHashMap可以支持16個併發更新線程,即這些線程可以在不導致鎖的爭用情況下進行併發更新。因此,ConcurrentHashMap可以支持比較高的併發性,並且其鎖的開銷一般比較小。ConcurrentHashMap中一個構造器支持concurrentcyLevel參數可以使我們調整ConcurrentHashMap支持的併發更新線程數。當然,既然這個值是可以調整的,那麼這個值就不會是越大或者越小就越好。這個值越大表示相應的開銷越大,越小標識它越可能導致併發更新時出現鎖的爭用。因此,concurrentcyLevel的值要調整也必須是根據實際需要來權衡。

CopyOnWriteArrayList是List接口的一個線程安全實現類,它相當於ArrayList的線程安全版。CopyOnWriteArrayList內部會維護一個實例變量array用於引用一個數組。該數組用於存儲列表的各個元素。CopyOnWriteArrayList的更新操作是通過創建一個新的數組newArray,並把老的數組的內容複製到newArray,然後對newArray進行更新並將array引用指向newArray。因此,array所引用的數組相當於當前CopyOnWriteArrayList實例的一個快照,而對CopyOnWriteArrayList的更新操作所導致的對象的複製(主要是對象引用的複製)的開銷相當於這個快照的間接開銷。CopyOnWriteArrayList所使用的遍歷方式就是快照。因此,CopyOnWriteArrayList適用於遍歷操作遠比更新操作頻繁或者不希望在遍歷的時候加鎖的場景。而在其他場景下,我們可能仍然要考慮使用Collections.synchronizedList()。

CopyOnWriteArraySet是Set接口的一個線程安全實現類,它相當於HashSet的線程安全版。CopyOnWriteArraySet內部實現使用了一個CopyOnWriteArrayList實例,因此CopyOnWriteArraySet的試用場景與CopyOnWriteArrayList相似。

發佈了35 篇原創文章 · 獲贊 3 · 訪問量 5967
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章