3.實戰java高併發程序設計--JDK併發包---3.3不要重複發明輪子:JDK的併發容器(工具類)

除了提供諸如同步控制、線程池等基本工具外,爲了提高開發人員的效率,JDK還爲大家準備了一大批好用的容器類,可以大大減少開發工作量。大家應該都聽說過一種說法,所謂程序就是“算法+數據結構”,這些容器類就是爲大家準備好的線程數據結構。你可以在裏面找到鏈表、HashMap、隊列等。當然,它們都是線程安全的。

在這裏,我也打算花一些篇幅爲大家介紹一下這些工具類。這些容器類的封裝都是非常完善並且“平易近人”的,也就是說只要你有那麼一點點的編程經驗,就可以非常容易地使用這些容器。因此,我可能會花更多的時間來分析這些工具的具體實現,希望起到拋磚引玉的作用。

3.3.1 超好用的工具類:併發集合簡介

JDK提供的這些容器大部分在java.util.concurrent包中。我先提綱挈領地介紹一下它們,初次露臉,大家只需要知道它們的作用即可。有關具體的實現和注意事項,在後面我會一一道來。

● ConcurrentHashMap:這是一個高效的併發HashMap。你可以把它理解爲一個線程安全的HashMap。

● CopyOnWriteArrayList:這是一個List,從名字看就知道它和ArrayList是一族的。在讀多寫少的場合,這個List的性能非常好,遠遠優於Vector。

● ConcurrentLinkedQueue:高效的併發隊列,使用鏈表實現。可以看作一個線程安全的LinkedList。

● BlockingQueue:這是一個接口,JDK內部通過鏈表、數組等方式實現了這個接口。表示阻塞隊列,非常適合作爲數據共享的通道。

● ConcurrentSkipListMap:跳錶的實現。這是一個Map,使用跳錶的數據結構進行快速查找。除以上併發包中的專有數據結構以外,java.util下的Vector是線程安全的(雖然性能和上述專用工具沒得比),另外Collections工具類可以幫助我們將任意集合包裝成線程安全的集合
3.3.2 線程安全的HashMap

在之前的章節中,已經給大家展示了在多線程環境中使用HashMap所帶來的問題,如果需要一個線程安全的HashMap應該怎麼做呢?一種可行的方法是使用Collections.synchronizedMap()方法包裝我們的HashMap。如下代碼,產生的HashMap就是線程安全的。

Collections.synchronizedMap()方法會生成一個名爲SynchronizedMap的Map。它使用委託,將自己所有Map相關的功能交給傳入的HashMap實現,而自己則主要負責保證線程安全。

具體參考下面的實現,首先SynchronizedMap內包裝了一個Map。在內部加鎖實現互斥操作,

雖然這個包裝的Map可以滿足線程安全的要求,但是它在多線程環境中的性能表現並不算太好。無論是對Map的讀取或者寫入,都需要獲得mutex的鎖,這會導致所有對Map的操作全部進入等待狀態,直到mutex鎖可用。如果併發級別不高,那麼一般也夠用。但是,在高併發環境中,我們有必要尋求新的解決方案

一個更加專業的併發HashMap是ConcurrentHashMap,它位於java.util.concurrent包內,專門爲併發進行了性能優化,因此更適合多線程的場合。

3.3.3 有關List的線程安全

隊列、鏈表之類的數據結構也是極其常用的,幾乎所有的應用程序都會與之相關。在Java中,ArrayList和Vector都使用數組作爲其內部實現。兩者最大的不同在於Vector是線程安全的,而ArrayList不是。此外,LinkedList使用鏈表的數據結構實現了List。但是很不幸,LinkedList並不是線程安全的,不過參考前面對HashMap的包裝,這裏我們也可以使用Collections.synchronizedList()方法來包裝任意List:

3.3.4 高效讀寫的隊列:深度剖析ConcurrentLinkedQueue類

隊列Queue也是常用的數據結構之一。在JDK中提供了一個ConcurrentLinkedQueue類用來實現高併發的隊列。從名字可以看到,這個隊列使用鏈表作爲其數據結構。有關ConcurrentLinkedQueue類的性能測試大家可以自行嘗試,這裏限於篇幅就不再給出性能測試的代碼了。大家只要知道ConcurrentLinkedQueue類應該算是在高併發環境中性能最好的隊列就可以了。它之所以能有很好的性能,是因爲其內部複雜的實現。

3.3.5 高效讀取:不變模式下的CopyOnWriteArrayList類

在很多應用場景中,讀操作可能會遠遠大於寫操作。比如,有些系統級別的信息,往往只需要加載或者修改很少的次數,但是會被系統內所有模塊頻繁訪問。對於這種場景,我們最希望看到的就是讀操作可以儘可能地快,而寫即使慢一些也沒有太大關

由於讀操作根本不會修改原有的數據,因此對於每次讀取都進行加鎖其實是一種資源浪費。我們應該允許多個線程同時訪問List的內部數據,畢竟讀取操作是安全的。根據讀寫鎖的思想,讀鎖和讀鎖之間確實也不衝突。但是,讀操作會受到寫操作的阻礙,當寫發生時,讀就必須等待,否則可能讀到不一致的數據。同理,當讀操作正在進行時,程序也不能進行寫入。

從這個類的名字我們可以看到,所謂CopyOnWrite就是在寫入操作時,進行一次自我複製。換句話說,當這個List需要修改時,我並不修改原有的內容(這對於保證當前在讀線程的數據一致性非常重要),而是對原有的數據進行一次複製,將修改的內容寫入副本中。寫完之後,再用修改完的副本替換原來的數據,這樣就可以保證寫操作不會影響讀了。需要注意的是:讀取代碼沒有任何同步控制和鎖操作,理由就是內部數組array不會發生修改,只會被另外一個array替換,因此可以保證數據安全。

3.3.6 數據共享通道:BlockingQueue

前面我們已經提到了ConcurrentLinkedQueue類是高性能的隊列。對於併發程序而言,高性能自然是一個我們需要追求的目標,但多線程的開發模式還會引入一個問題,那就是如何進行多個線程間的數據共享呢?比如,線程A希望給線程B發一條消息,用什麼方式告知線程B是比較合理的呢?一般來說,我們總是希望整個系統是鬆散耦合的。比如,你所在小區的物業希望可以得到一些業主的意見,設立了一個意見箱,如果對物業有任何要求或者意見都可以投到意見箱裏。作爲業主的你並不需要直接找到物業相關的工作人員就能表達意見。實際上,物業的工作人員也可能經常發生變動,直接找工作人員未必是一件方便的事情。而你投遞到意見箱的意見總是會被物業的工作人員看到,不管是否發生了人員的變動。這樣你就可以很容易地表達自己的訴求了。你既不需要直接和他們對話,又可以輕鬆提出自己的建議(這裏假定物業公司的員工都是盡心盡責的好員工)。

將這個模式映射到我們程序中,就是說我們既希望線程A能夠通知線程B,又希望線程A不知道線程B的存在。

這樣,如果將來進行重構或者升級,我們完全可以不修改線程A,而直接把線程B升級爲線程C,保證系統的平滑過渡。而這中間的“意見箱”就可以使用BlockingQueue來實現。'

與之前提到的ConcurrentLinkedQueue類或者CopyOnWriteArrayList類不同,BlockingQueue是一個接口,並非一個具體的實現。它的主要實現有下面一些,如圖3.17所示。

 

而BlockingQueue之所以適合作爲數據共享的通道,其關鍵還在於Blocking上。Blocking是阻塞的意思,當服務線程(服務線程指不斷獲取隊列中的消息,進行處理的線程)處理完成隊列中所有的消息後,它如何知道下一條消息何時到來呢?一種最簡單的做法是讓這個線程按照一定的時間間隔不停地循環和監控這個隊列

一種最簡單的做法是讓這個線程按照一定的時間間隔不停地循環和監控這個隊列。這是一種可行的方案,但顯然造成了不必要的資源浪費,而且循環週期也難以確定。BlockingQueue很好地解決了這個問題。它會讓服務線程在隊列爲空時進行等待,當有新的消息進入隊列後,自動將線程喚醒,如圖3.18所示。那它是如何實現的呢?我們以ArrayBlockingQueue類爲例,來一探究竟。

3.3.7 隨機數據結構:跳錶(SkipList)

在JDK的併發包中,除常用的哈希表外,還實現了一種有趣的數據結構—跳錶。跳錶是一種可以用來快速查找的數據結構,有點類似於平衡樹。它們都可以對元素進行快速查找。但一個重要的區別是:對平衡樹的插入和刪除往往很可能導致平衡樹進行一次全局的調整,而對跳錶的插入和刪除只需要對整個數據結構的局部進行操作即可。這樣帶來的好處是:在高併發的情況下,你會需要一個全局鎖來保證整個平衡樹的線程安全。而對於跳錶,你只需要部分鎖即可。這樣,在高併發環境下,你就可以擁有更好的性能。就查詢的性能而言,因爲跳錶的時間複雜度是O(logn),所以在併發數據結構中,JDK使用跳錶來實現一個Map。

跳錶的另外一個特點是隨機算法。跳錶的本質是同時維護了多個鏈表,並且鏈表是分層的。圖3.19是跳錶結構示意圖。

底層的鏈表維護了跳錶內所有的元素,每上面一層鏈表都是下面一層鏈表的子集,一個元素插入哪些層是完全隨機的。因此,如果運氣不好,你可能會得到一個性能很糟糕的結構。但是在實際工作中,它的表現是非常好的。

使用跳錶實現Map和使用哈希算法實現Map的另外一個不同之處是:哈希並不會保存元素的順序,而跳錶內所有的元素都是有序的。因此在對跳錶進行遍歷時,你會得到一個有序的結果。因此,如果你的應用需要有序性,那麼跳錶就是你的最佳選擇。

實現這一數據結構的類是ConcurrentSkipListMap。下面展示了跳錶的簡單使用方法

和HashMap不同,對跳錶的遍歷輸出是有序的


 

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