在Java類庫中出現的第一個關聯的集合類是Hashtable,它是JDK 1.0的一部分。Hashtable提供了一種易於使用的、線程安全的、關聯的map功能,這固然也是方便的。然而,線程安全性是憑代價換來的――Hashtable的全部方法都是同步的。此時,無競爭的同步會致使可觀的性能代價。Hashtable的後繼者HashMap是做爲JDK1.2中的集合框架的一部分出現的,它經過提供一個不一樣步的基類和一個同步的包裝器Collections.synchronizedMap,解決了線程安全性問題。經過將基本的功能從線程安全性中分離開來,Collections.synchronizedMap容許須要同步的用戶能夠擁有同步,而不須要同步的用戶則沒必要爲同步付出代價。 java
Hashtable和synchronizedMap所採起的得到同步的簡單方法(同步Hashtable中或者同步的Map包裝器對象中的每一個方法)有兩個主要的不足。首先,這種方法對於可伸縮性是一種障礙,由於一次只能有一個線程能夠訪問hash表。同時,這樣仍不足以提供真正的線程安全性,許多公用的混合操做仍然須要額外的同步。雖然諸如get()和put()之類的簡單操做能夠在不須要額外同步的狀況下安全地完成,但仍是有一些公用的操做序列,例如迭代或者put-if-absent(空則放入),須要外部的同步,以免數據爭用。 數據庫
同步的集合包裝器synchronizedMap和synchronizedList,有時也被稱做 有條件地線程安全――全部單個的操做都是線程安全的,可是多個操做組成的操做序列卻可能致使數據爭用,由於在操做序列中控制流取決於前面操做的結果。 清單1中第一片斷展現了公用的put-if-absent語句塊――若是一個條目不在Map中,那麼添加這個條目。不幸的是,在containsKey()方法返回到put()方法被調用這段時間內,可能會有另外一個線程也插入一個帶有相同鍵的值。若是您想確保只有一次插入,您須要用一個對Map m進行同步的同步塊將這一對語句包裝起來。 緩存
清單1中其餘的例子與迭代有關。在第一個例子中,List.size()的結果在循環的執行期間可能會變得無效,由於另外一個線程能夠從這個列表中刪除條目。若是時機不得當,在恰好進入循環的最後一次迭代以後有一個條目被另外一個線程刪除了,則List.get()將返回null,而doSomething()則極可能會拋出一個NullPointerException異常。那麼,採起什麼措施才能避免這種狀況呢?若是當您正在迭代一個List時另外一個線程也可能正在訪問這個List,那麼在進行迭代時您必須使用一個synchronized塊將這個List包裝起來,在List1 上同步,從而鎖住整個List。這樣作雖然解決了數據爭用問題,可是在併發性方面付出了更多的代價,由於在迭代期間鎖住整個List會阻塞其餘線程,使它們在很長一段時間內不能訪問這個列表。 安全
集合框架引入了迭代器,用於遍歷一個列表或者其餘集合,從而優化了對一個集合中的元素進行迭代的過程。然而,在java.util集合類中實現的迭代器極易崩潰,也就是說,若是在一個線程正在經過一個Iterator遍歷集合時,另外一個線程也來修改這個集合,那麼接下來的Iterator.hasNext()或Iterator.next()調用將拋出ConcurrentModificationException異常。就拿剛纔這個例子來說,若是想要防止出現ConcurrentModificationException異常,那麼當您正在進行迭代時,您必須使用一個在List l上同步的synchronized塊將該List包裝起來,從而鎖住整個List。(或者,您也能夠調用List.toArray(),在不一樣步的狀況下對數組進行迭代,可是若是列表比較大的話這樣作代價很高)。 服務器
清單 1. 同步的map中的公用競爭條件
Map m = Collections.synchronizedMap(new HashMap());
List l = Collections.synchronizedList(new ArrayList());
// put-if-absent idiom -- contains a race condition
// may require external synchronization
if (!map.containsKey(key))
map.put(key, value);
// ad-hoc iteration -- contains race conditions
// may require external synchronization
for (int i=0; i<list.size(); i++) {
doSomething(list.get(i));
}
// normal iteration -- can throw ConcurrentModificationException
// may require external synchronization
for (Iterator i=list.iterator(); i.hasNext(); ) {
doSomething(i.next());
} 併發
synchronizedList和synchronizedMap提供的有條件的線程安全性也帶來了一個隱患――開發者會假設,由於這些集合都是同步的,因此它們都是線程安全的,這樣一來他們對於正確地同步混合操做這件事就會疏忽。其結果是儘管表面上這些程序在負載較輕的時候可以正常工做,可是一旦負載較重,它們就會開始拋出NullPointerException或ConcurrentModificationException。 函數
可伸縮性指的是一個應用程序在工做負載和可用處理資源增長時其吞吐量的表現狀況。一個可伸縮的程序可以經過使用更多的處理器、內存或者I/O帶寬來相應地處理更大的工做負載。鎖住某個共享的資源以得到獨佔式的訪問這種作法會造成可伸縮性瓶頸――它使其餘線程不能訪問那個資源,即便有空閒的處理器能夠調用那些線程也無濟於事。爲了取得可伸縮性,咱們必須消除或者減小咱們對獨佔式資源鎖的依賴。
同步的集合包裝器以及早期的Hashtable和Vector類帶來的更大的問題是,它們在單個的鎖上進行同步。這意味着一次只有一個線程能夠訪問集合,若是有一個線程正在讀一個Map,那麼全部其餘想要讀或者寫這個Map的線程就必須等待。最多見的Map操做,get()和put(),可能比表面上要進行更多的處理――當遍歷一個hash表的bucket以期找到某一特定的key時,get()必須對大量的候選bucket調用Object.equals()。若是key類所使用的hashCode()函數不能將value均勻地分佈在整個hash表範圍內,或者存在大量的hash衝突,那麼某些bucket鏈就會比其餘的鏈長不少,而遍歷一個長的hash鏈以及對該hash鏈上必定百分比的元素調用equals()是一件很慢的事情。在上述條件下,調用get()和put()的代價高的問題不只僅是指訪問過程的緩慢,並且,當有線程正在遍歷那個hash鏈時,全部其餘線程都被鎖在外面,不能訪問這個Map。
(哈希表根據一個叫作hash的數字關鍵字(key)將對象存儲在bucket中。hash value是從對象中的值計算得來的一個數字。每一個不一樣的hash value都會建立一個新的bucket。要查找一個對象,您只須要計算這個對象的hash value並搜索相應的bucket就好了。經過快速地找到相應的bucket,就能夠減小您須要搜索的對象數量了。譯者注)
get()執行起來可能會佔用大量的時間,而在某些狀況下,前面已經做了討論的有條件的線程安全性問題會讓這個問題變得還要糟糕得多。 清單1 中演示的爭用條件經常使得對單個集合的鎖在單個操做執行完畢以後還必須繼續保持一段較長的時間。若是您要在整個迭代期間都保持對集合的鎖,那麼其餘的線程就會在鎖外停留很長的一段時間,等待解鎖。
Map在服務器應用中最多見的應用之一就是實現一個cache。服務器應用可能須要緩存文件內容、生成的頁面、數據庫查詢的結果、與通過解析的XML文件相關的DOM樹,以及許多其餘類型的數據。cache的主要用途是重用前一次處理得出的結果以減小服務時間和增長吞吐量。cache工做負載的一個典型的特徵就是檢索大大多於更新,所以(理想狀況下)cache可以提供很是好的get()性能。不過,使用會妨礙性能的cache還不如徹底不用cache。
若是使用synchronizedMap來實現一個cache,那麼您就在您的應用程序中引入了一個潛在的可伸縮性瓶頸。由於一次只有一個線程能夠訪問Map,這些線程包括那些要從Map中取出一個值的線程以及那些要將一個新的(key, value)對插入到該map中的線程。
提升HashMap的併發性同時還提供線程安全性的一種方法是廢除對整個表使用一個鎖的方式,而採用對hash表的每一個bucket都使用一個鎖的方式(或者,更常見的是,使用一個鎖池,每一個鎖負責保護幾個bucket)。這意味着多個線程能夠同時地訪問一個Map的不一樣部分,而沒必要爭用單個的集合範圍的鎖。這種方法可以直接提升插入、檢索以及移除操做的可伸縮性。不幸的是,這種併發性是以必定的代價換來的――這使得對整個集合進行操做的一些方法(例如size()或isEmpty())的實現更加困難,由於這些方法要求一次得到許多的鎖,而且還存在返回不正確的結果的風險。然而,對於某些狀況,例如實現cache,這樣作是一個很好的折衷――由於檢索和插入操做比較頻繁,而size()和isEmpty()操做則少得多。
util.concurrent包中的ConcurrentHashMap類(也將出如今JDK 1.5中的java.util.concurrent包中)是對Map的線程安全的實現,比起synchronizedMap來,它提供了好得多的併發性。多個讀操做幾乎總能夠併發地執行,同時進行的讀和寫操做一般也能併發地執行,而同時進行的寫操做仍然能夠不時地併發進行(相關的類也提供了相似的多個讀線程的併發性,可是,只容許有一個活動的寫線程)。ConcurrentHashMap被設計用來優化檢索操做;實際上,成功的get()操做完成以後一般根本不會有鎖着的資源。要在不使用鎖的狀況下取得線程安全性須要必定的技巧性,而且須要對Java內存模型(Java Memory Model)的細節有深刻的理解。ConcurrentHashMap實現,加上util.concurrent包的其餘部分,已經被研究正確性和線程安全性的併發專家所正視。在下個月的文章中,咱們將看看ConcurrentHashMap的實現的細節。
ConcurrentHashMap經過稍微地鬆弛它對調用者的承諾而得到了更高的併發性。檢索操做將能夠返回由最近完成的插入操做所插入的值,也能夠返回在步調上是併發的插入操做所添加的值(可是決不會返回一個沒有意義的結果)。由ConcurrentHashMap.iterator()返回的Iterators將每次最多返回一個元素,而且決不會拋出ConcurrentModificationException異常,可是可能會也可能不會反映在該迭代器被構建以後發生的插入操做或者移除操做。在對集合進行迭代時,不須要表範圍的鎖就能提供線程安全性。在任何不依賴於鎖整個表來防止更新的應用程序中,可使用ConcurrentHashMap來替代synchronizedMap或Hashtable。
上述改進使得ConcurrentHashMap可以提供比Hashtable高得多的可伸縮性,並且,對於不少類型的公用案例(好比共享的cache)來講,還不用損失其效率。
表 1對Hashtable和ConcurrentHashMap的可伸縮性進行了粗略的比較。在每次運行過程當中, n 個線程併發地執行一個死循環,在這個死循環中這些線程從一個Hashtable或者ConcurrentHashMap中檢索隨機的key value,發如今執行put()操做時有80%的檢索失敗率,在執行操做時有1%的檢索成功率。測試所在的平臺是一個雙處理器的Xeon系統,操做系統是Linux。數據顯示了10,000,000次迭代以毫秒計的運行時間,這個數據是在將對ConcurrentHashMap的操做標準化爲一個線程的狀況下進行統計的。您能夠看到,當線程增長到多個時,ConcurrentHashMap的性能仍然保持上升趨勢,而Hashtable的性能則隨着爭用鎖的狀況的出現而當即降了下來。
比起一般狀況下的服務器應用,此次測試中線程的數量看上去有點少。然而,由於每一個線程都在不停地對錶進行操做,因此這與實際環境下使用這個表的更多數量的線程的爭用狀況基本等同。
表 1.Hashtable 與 ConcurrentHashMap在可伸縮性方面的比較
線程數 ConcurrentHashMap Hashtable 1 1.00 1.03 2 2.59 32.40 4 5.58 78.23 8 13.21 163.48 16 27.58 341.21 32 57.27 778.41
在那些遍歷操做大大地多於插入或移除操做的併發應用程序中,通常用CopyOnWriteArrayList類替代ArrayList。若是是用於存放一個偵聽器(listener)列表,例如在AWT或Swing應用程序中,或者在常見的JavaBean中,那麼這種狀況很常見(相關的CopyOnWriteArraySet使用一個CopyOnWriteArrayList來實現Set接口)。
若是您正在使用一個普通的ArrayList來存放一個偵聽器列表,那麼只要該列表是可變的,並且可能要被多個線程訪問,您就必需要麼在對其進行迭代操做期間,要麼在迭代前進行的克隆操做期間,鎖定整個列表,這兩種作法的開銷都很大。當對列表執行會引發列表發生變化的操做時,CopyOnWriteArrayList並非爲列表建立一個全新的副本,它的迭代器確定可以返回在迭代器被建立時列表的狀態,而不會拋出ConcurrentModificationException。在對列表進行迭代以前沒必要克隆列表或者在迭代期間鎖定列表,由於迭代器所看到的列表的副本是不變的。換句話說,CopyOnWriteArrayList含有對一個不可變數組的一個可變的引用,所以,只要保留好那個引用,您就能夠得到不可變的線程安全性的好處,並且不用鎖定列表。
同步的集合類Hashtable和Vector,以及同步的包裝器類Collections.synchronizedMap和Collections.synchronizedList,爲Map和List提供了基本的有條件的線程安全的實現。然而,某些因素使得它們並不適用於具備高度併發性的應用程序中――它們的集合範圍的單鎖特性對於可伸縮性來講是一個障礙,並且,不少時候還必須在一段較長的時間內鎖定一個集合,以防止出現ConcurrentModificationExceptions異常。ConcurrentHashMap和CopyOnWriteArrayList實現提供了更高的併發性,同時還保住了線程安全性,只不過在對其調用者的承諾上打了點折扣。ConcurrentHashMap和CopyOnWriteArrayList並非在您使用HashMap或ArrayList的任何地方都必定有用,可是它們是設計用來優化某些特定的公用解決方案的。許多併發應用程序將從對它們的使用中得到好處。