Java多線程阻塞隊列和併發集合

Java多線程 阻塞隊列和併發集合

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

3.1 BlockingQueue接口

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

       BlockingQueueQueue接口基礎上提供了額外的兩種類型的操作,分別是獲取元素時等待隊列變爲非空和添加元素時等待空間變爲可用。

       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給出了阻塞隊列的實現類結構。

Graphic 3-1 BlockingQueue接口

Java多線程 <wbr>阻塞隊列和併發集合

 

Graphic3-2阻塞隊列的實現類

Java多線程 <wbr>阻塞隊列和併發集合

 

3.1.1 ArrayBlockingQueue

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

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

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

3.1.2 LinkedBlockingQueue

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

3.1.3 PriorityBlockingQueue

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

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

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

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

       PriorityBlockingQueue的實現過程中聚合了PriorityQueue的一個實例,並且優先隊列的操作完全依賴與PriorityQueue的實現。在PriorityQueue中使用了一個一維數組來存儲相關的元素信息。一維數組使用最小堆算法進行元素添加。

       Graphic3-3PriorityBlockingQueue的類關係

Java多線程 <wbr>阻塞隊列和併發集合      

3.1.4 DelayQueue

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

3.2 併發集合

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

3.2.1 ConcurrentMap接口

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

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

      

Graphic3-4 ConcurrentMap

Java多線程 <wbr>阻塞隊列和併發集合

 

Graphic3-5 實現ConcurrentMap接口。

Java多線程 <wbr>阻塞隊列和併發集合

 

3.2.1.1 TreeMap

       儘管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.2 ConcurrentSkipListMap

       另外一種實現了SortedMap接口的映射表是ConcurrentSkipListMapConcurrentSkipListMap提供了一種線程安全的併發訪問的排序映射表。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.synchronizedSortedMapTreeMap進行包裝,也可以提供較好的效率。對於高併發程序,應當使用ConcurrentSkipListMap,能夠提供更高的併發度。

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

       注意,調用ConcurrentSkipListMapsize時,由於多個線程可以同時對映射表進行操作,所以映射表需要遍歷整個鏈表才能返回元素個數,這個操作是個O(log(n))的操作。

Graphic3-6 SkipList示例

 

  Java多線程 <wbr>阻塞隊列和併發集合    

3.2.1.3 HashMap

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

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

3.2.1.4 ConcurrentHashMap

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

3.2.2 ConcurrentSkipListSet

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

3.2.3 CopyOnWriteArrayList

       CopyOnWriteArrayListArrayList的一個線程安全的變體,其中對於所有的可變操作都是通過對底層數組進行一次新的複製來實現的。

       由於可變操作需要對底層的數據進行一次完全拷貝,因此開銷一般較大,但是當遍歷操作遠遠多於可變操作時,此方法將會更有效,這是一種被稱爲“快照”的模式,數組在迭代器生存期內不會發生更改,因此不會產生衝突。創建迭代器後,迭代器不會反映列表的添加、移除或者更改。不支持在迭代器上進行removesetadd操作。CopyOnWriteArraySetCopyOnWriteArrayList相似,只不過是Set類的一個變體。

3.2.3 Collections提供的線程安全的封裝

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

3.2.4 簡單總結

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

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

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