1. 程式人生 > >Java多執行緒 阻塞佇列和併發集合

Java多執行緒 阻塞佇列和併發集合

本章主要探討在多執行緒程式中與集合相關的內容。在多執行緒程式中,如果使用普通集合往往會造成資料錯誤,甚至造成程式崩潰。Java為多執行緒專門提供了特有的執行緒安全的集合類,通過下面的學習,您需要掌握這些集合的特點是什麼,底層實現如何、在何時使用等問題。

3.1BlockingQueue介面

java阻塞佇列應用於生產者消費者模式、訊息傳遞、並行任務執行和相關併發設計的大多數常見使用上下文。

      BlockingQueue在Queue介面基礎上提供了額外的兩種型別的操作,分別是獲取元素時等待佇列變為非空和新增元素時等待空間變為可用。

      BlockingQueue新增操作的四種形式:

Java多執行緒 <wbr>阻塞佇列和併發集合

      插入操作是指向佇列中新增一個元素,至於元素存放的位置與具體佇列的實現有關。移除操作將會移除佇列的頭部元素,並將這個移除的元素作為返回值反饋給呼叫者。檢查操作是指返回佇列的頭元素給呼叫者,佇列不對這個頭元素進行刪除處理。

      丟擲異常形式的操作,在佇列已滿的情況下,呼叫add方法將會丟擲IllegalStateException異常。如果呼叫remove方法時,佇列已經為空,則丟擲一個NoSuchElementException異常。(實際上,remove方法還可以附帶一個引數,用來刪除佇列中的指定元素,如果這個元素不存在,也會丟擲NoSuchElementException異常)。如果呼叫element檢查頭元素,佇列為空時,將會丟擲NoSuchElementException異常。

      特殊值操作與丟擲異常不同,在出錯的時候,返回一個空指標,而不會丟擲異常。

      阻塞形式的操作,呼叫put方法時,如果佇列已滿,則呼叫執行緒阻塞等待其它執行緒從佇列中取出元素。呼叫take方法時,如果阻塞佇列已經為空,則呼叫執行緒阻塞等待其它執行緒向佇列新增新元素。

      超時形式操作,在阻塞的基礎上新增一個超時限制,如果等待時間超過指定值,丟擲InterruptedException。

      阻塞佇列實現了Queue介面,而Queue介面實現了Collection介面,因此BlockingQueue也提供了remove(e)操作,即從佇列中移除任意指定元素,但是這個操作往往不會按預期那樣高效的執行,所以應當儘量少的使用這種操作。

      阻塞佇列與併發佇列(例如ConcurrentLinkQueue)都是執行緒安全的,但使用的場合不同。

      Graphic3-1給出了阻塞佇列的介面方法,Graphic3-2給出了阻塞佇列的實現類結構。

Graphic3-1 BlockingQueue介面

Java多執行緒 <wbr>阻塞佇列和併發集合

Graphic3-2阻塞佇列的實現類

Java多執行緒 <wbr>阻塞佇列和併發集合

3.1.1ArrayBlockingQueue類

      一個以陣列為基礎的有界阻塞佇列,此佇列按照先進先出原則對元素進行排序。佇列頭部元素是佇列中存在時間最長的元素,佇列尾部是存在時間最短的元素,新元素將會被插入到佇列尾部。佇列從頭部開始獲取元素。

      ArrayBlockingQueue是“有界快取區”模型的一種實現,一旦建立了這樣的快取區,就不能再改變緩衝區的大小。ArrayBlockingQueue的一個特點是,必須在建立的時候指定佇列的大小。當緩衝區已滿,則需要阻塞新增的插入操作,同理,當緩衝區已空需要阻塞新增的提取操作。

      ArrayBlockingQueue是使用的是迴圈佇列方法實現的,對ArrayBlockingQueue的相關操作的時間複雜度,可以參考迴圈佇列進行分析。

3.1.2LinkedBlockingQueue

      一種通過連結串列實現的阻塞佇列,支援先進先出。佇列的頭部是佇列中保持時間最長的元素,佇列的尾部是保持時間最短的元素。新元素插入佇列的尾部。可選的容量設定可以有效防止佇列過於擴張造成系統資源的過多消耗,如果不指定佇列容量,佇列預設使用Integer.MAX_VALUE。LinkedBlockingQueue的特定是,支援無限(理論上)容量。

3.1.3PriorityBlockingQueue

      PriorityBlockingQueue是一種基於優先順序進行排隊的無界佇列。佇列中的元素按照其自然順序進行排列,或者根據提供的Comparator進行排序,這與構造佇列時,提供的引數有關。

      使用提取方法時,佇列將返回頭部,具有最高優先順序(或最低優先順序,這與排序規則有關)的元素。如果多個元素具有相同的優先順序,則同等優先順序間的元素獲取次序無特殊說明。

      優先順序佇列使用的是一種可擴充套件的陣列結構,一般可以認為這個佇列是無界的。當需要新新增一個元素時,如果此時陣列已經被填滿,優先佇列將會自動擴充當前陣列(一般認為是,先分配一個原陣列一定倍數空間的陣列,之後將原陣列中的元素拷貝到新分配的陣列中,釋放原陣列的空間)。

      如果使用優先順序佇列的iterator變數佇列時,不保證遍歷次序按照優先順序大小進行。因為優先順序佇列使用的是堆結構。如果需要按照次序遍歷需要使用Arrays.sort(pq.toArray())。關於堆結構的相關演算法,請查考資料結構相關的書籍。

      在PriorityBlockingQueue的實現過程中聚合了PriorityQueue的一個例項,並且優先佇列的操作完全依賴與PriorityQueue的實現。在PriorityQueue中使用了一個一維陣列來儲存相關的元素資訊。一維陣列使用最小堆演算法進行元素新增。

      Graphic3-3PriorityBlockingQueue的類關係

Java多執行緒 <wbr>阻塞佇列和併發集合      

3.1.4DelayQueue

      一個無界阻塞佇列,只有在延時期滿時才能從中提取元素。如果沒有元素到達延時期,則沒有頭元素。

3.2 併發集合

      在多執行緒程式中使用的集合類,與普通程式中使用的集合類是不同的。因為有可能多個執行緒同時訪問或修改同一集合,如果使用普通集合,很可能造成相應操作出現差錯,甚至崩潰。Java提供了用於執行緒訪問安全的集合。(前面討論的BlockingQueue也是這裡集合中的一種)。下面針對這些集合,以及集合中使用的相應演算法進行探討。在設計演算法時,僅對相應演算法進行簡要說明,如果讀者需要深入瞭解這些演算法的原理,請參考其他的高階資料結構相關的書籍。

3.2.1ConcurrentMap介面

      ConcurrentMap介面在Map介面的基礎上提供了一種執行緒安全的方法訪問機制。ConcurrentMap介面額外提供了多執行緒使用的四個方法,這四個方法實際是對Map已有方法的一個組合,並對這種組合提供一種原子操作。Graphic3-4給出了ConcurrentMap相關的操作。Graphic3-5給出了ConcurrentMap的實現類關係圖。

      從Graphic3-5中可以看出ConcurrentNavigableMap繼承自ConcurrentMap,ConcurrentNavigableMap是一種SortedMap,就是說,對映中的元素會根據鍵值進行排序的。在java.util類庫中,有兩個類實現了SortedMap介面,分別是TreeMap和ConcurrentSkipListMap。TreeMap使用的是紅黑樹結構。而ConcurrentSkipListMap使用作為底層實現的SkipList(翻譯為跳錶)資料結構。此外ConcurrentHashMap實現了ConcurrentMap介面,使用的是HashMap方法。

Graphic3-4 ConcurrentMap

Java多執行緒 <wbr>阻塞佇列和併發集合

Graphic3-5 實現ConcurrentMap介面。

Java多執行緒 <wbr>阻塞佇列和併發集合

3.2.1.1TreeMap

      儘管TreeMap不是執行緒安全的,但是基於其資料結構的複雜性和方便對比說明,還是在這裡簡單提一下。TreeMap實現了SortedMap介面。TreeMap使用的是紅黑樹(這是高等資料結構中的一種),在紅黑樹演算法中,當新增或刪除節點時,需要進行旋轉調整樹的高度。使用紅黑樹演算法具有較好的操作特性,插入、刪除、查詢都能在O(log(n))時間內完成。紅黑樹理論和實現是很複雜的,但可以帶來較高的效率,因此在許多場合也得到了廣泛使用。紅黑樹的一個缺陷在於,可變操作很可能影響到整棵樹的結構,針對修改的區域性效果不好。相關演算法請參考http://blog.sina.com.cn/s/blog_616e189f0100qgcm.html

      TreeMap不是執行緒安全的,如果同時有多個執行緒訪問同一個Map,並且其中至少有一個執行緒從結構上修改了該對映,則必須使用外部同步。可以使用Collections.synchronizedSortedMap方法來包裝該對映。(注意使用包裝器包裝的SortMap是執行緒安全的,但不是併發的,效率上很可能遠遠不及ConcurrentSkipListMap,因此使用包裝器的方法並不十分推薦,有人認為那是一種過時的做法。包裝器使用了鎖機制控制對Map的併發訪問,但是這種加鎖的粒度可能過大,很可能影響併發度)。

3.2.1.2ConcurrentSkipListMap

      另外一種實現了SortedMap介面的對映表是ConcurrentSkipListMap。ConcurrentSkipListMap提供了一種執行緒安全的併發訪問的排序對映表。SkipList(跳錶)結構,在理論上能夠在O(log(n))時間內完成查詢、插入、刪除操作。SkipList是一種紅黑樹的替代方案,由於SkipList與紅黑樹相比無論從理論和實現都簡單許多,所以得到了很好的推廣。SkipList是基於一種統計學原理實現的,有可能出現最壞情況,即查詢和更新操作都是O(n)時間複雜度,但從統計學角度分析這種概率極小。Graphic3-6給出了SkipList的資料表示示例。有關skipList更多的說明可以參考:http://blog.csdn.net/caoeryingzi/archive/2010/11/18/6018070.aspxhttp://en.wikipedia.org/wiki/Skip_list這裡不在累述。希望讀者自行學習。

      使用SkipList型別的資料結構更容易控制多執行緒對集合訪問的處理,因為連結串列的區域性處理性比較好,當多個執行緒對SkipList進行更新操作(指插入和刪除)時,SkipList具有較好的區域性性,每個單獨的操作,對整體資料結構影響較小。而如果使用紅黑樹,很可能一個更新操作,將會波及整個樹的結構,其區域性性較差。因此使用SkipList更適合實現多個執行緒的併發處理。在非多執行緒的情況下,應當儘量使用TreeMap,因為似乎紅黑樹結構要比SkipList結構執行效率略優(無論是時間複雜度還是空間複雜度,作者沒有做夠測試,只是直覺)。此外對於併發性相對較低的並行程式可以使用Collections.synchronizedSortedMap將TreeMap進行包裝,也可以提供較好的效率。對於高併發程式,應當使用ConcurrentSkipListMap,能夠提供更高的併發度。

      所以在多執行緒程式中,如果需要對Map的鍵值進行排序時,請儘量使用ConcurrentSkipListMap,可能得到更好的併發度。

      注意,呼叫ConcurrentSkipListMap的size時,由於多個執行緒可以同時對對映表進行操作,所以對映表需要遍歷整個連結串列才能返回元素個數,這個操作是個O(log(n))的操作。

Graphic3-6 SkipList示例

  Java多執行緒 <wbr>阻塞佇列和併發集合    

3.2.1.3HashMap類

      對Map類的另外一個實現是HashMap。HashMap使用Hash表資料結構。HashMap假定雜湊函式能夠將元素適當的分佈在各桶之間,提供一種接近O(1)的查詢和更新操作。但是如果需要對集合進行迭代,則與HashMap的容量和桶的大小有關,因此HashMap的迭代效率不會很高(尤其是你為HashMap設定了較大的容量時)。

      與HashMap效能有影響的兩個引數是,初始容量和載入因子。容量是雜湊表中桶的數量,初始容量是雜湊表在建立時的容量。載入因子是雜湊表在容器容量被自動擴充之前,HashMap能夠達到多滿的一種程度。當hash表中的條目數超出了載入因子與當前容量的乘積時,Hash表需要進行rehash操作,此時Hash表將會擴充為以前兩倍的桶數,這個擴充過程需要進行完全的拷貝工作,效率並不高,因此應當儘量避免。合理的設定Hash表的初始容量和載入因子會提高Hash表的效能。HashMap自身不是執行緒安全的,可以通過Collections的synchronizedMap方法對HashMap進行包裝。

3.2.1.4ConcurrentHashMap類

      ConcurrentHashMap類實現了ConcurrentMap介面,並提供了與HashMap相同的規範和功能。實際上Hash表具有很好的區域性可操作性,因為對Hash表的更新操作僅會影響到具體的某個桶(假設更新操作沒有引發rehash),對全域性並沒有顯著影響。因此ConcurrentHashMap可以提供很好的併發處理能力。可以通過concurrencyLevel的設定,來控制併發工作執行緒的數目(預設為16),合理的設定這個值,有時很重要,如果這個值設定的過高,那麼很有可能浪費空間和時間,使用的值過低,又會導致執行緒的爭用,對數量估計的過高或過低往往會帶來明顯的效能影響。最好在建立ConcurrentHashMap時提供一個合理的初始容量,畢竟rehash操作具有較高的代價。

3.2.2ConcurrentSkipListSet類

      實際上Set和Map從結構來說是很像的,從底層的演算法原理分析,Set和Map應當屬於同源的結構。所以Java也提供了TreeSet和ConcurrentSkipListSet兩種SortedSet,分別適合於非多執行緒(或低併發多執行緒)和多執行緒程式使用。具體的演算法請參考前述的Map相關介紹,這裡不在累述。

3.2.3CopyOnWriteArrayList類

      CopyOnWriteArrayList是ArrayList的一個執行緒安全的變體,其中對於所有的可變操作都是通過對底層陣列進行一次新的複製來實現的。

      由於可變操作需要對底層的資料進行一次完全拷貝,因此開銷一般較大,但是當遍歷操作遠遠多於可變操作時,此方法將會更有效,這是一種被稱為“快照”的模式,陣列在迭代器生存期內不會發生更改,因此不會產生衝突。建立迭代器後,迭代器不會反映列表的新增、移除或者更改。不支援在迭代器上進行remove、set和add操作。CopyOnWriteArraySet與CopyOnWriteArrayList相似,只不過是Set類的一個變體。

3.2.3Collections提供的執行緒安全的封裝

      Collections中提供了synchronizedCollection、synchronizedList、synchronizedMap、synchronizedSet、synchronizedSortedMap、synchronizedSortedMap等方法可以完成多種集合的執行緒安全的包裝,如果在併發度不高的情況下,可以考慮使用這些包裝方法,不過由於Concurrent相關的類的出現,已經不這麼提倡使用這些封裝了,這些方法有些人稱他們為過時的執行緒安全機制。

3.2.4 簡單總結

      提供執行緒安全的集合簡單概括分為三類,首先,對於併發性要求很高的需求可以選擇以Concurrent開頭的相應的集合類,這些類主要包括:ConcurrentHashMap、ConcurrentLinkedQueue、ConcurrentSkipListMap、ConcurrentSkipSet。其次對於可變操作次數遠遠小於遍歷的情況,可以使用CopyOnWriteArrayList和CopyOnWriteArraySet類。最後,對於併發規模比較小的並行需求可以選擇Collections類中的相應方法對已有集合進行封裝。

      此外,本章還對一些集合類的底層實現進行簡單探討,對底層實現的瞭解有利於對何時使用何種方式作出正確判斷。希望大家能夠將涉及到原理(主要有迴圈佇列、堆、HashMap、紅黑樹、SkipList)進行仔細研究,這樣才能更深入瞭解Java為什麼這樣設計類庫,在什麼情況使用,應當如何使用。