volatile, synchronized, 讀寫屏障,CONCURRENTHASHMAP

Java 理論與實踐: 構建一個更好的 HashMap

ConcurrentHashMap 如何在不損失線程安全的同時提供更高的併發性html

Brian Goetz, 首席顧問, Quiotix Corp

簡介: ConcurrentHashMap 是 Doug Lea 的 util.concurrent 包的一部分,它提供比 Hashtable 或者 synchronizedMap 更高程度的併發性。並且,對於大多數成功的 get() 操做它會設法避免徹底鎖定,其結果就是使得併發應用程序有着很是好的吞吐量。這個月,Brian Goetz 仔細分析了 ConcurrentHashMap 的代碼,並探討 Doug Lea 是如何在不損失線程安全的狀況下取得這麼驕人成績的。請在 討論論壇 上與做者及其餘讀者共享您對本文的一些想法(也能夠在文章的頂部或底部點擊 討論 來訪問論壇)。java

查看本系列更多內容node

發佈日期: 2003 年 8 月 29 日
級別: 中級
訪問狀況 2997 次瀏覽
建議: 1 (查看或添加評論) 緩存

1 star 2 stars 3 stars 4 stars 5 stars 平均分 (共 3 個評分 )

在7月份的那期 Java理論與實踐(「併發集合類」)中,咱們簡單地回顧了可伸縮性的瓶頸,並討論了怎麼用共享數據結構的方法得到更高的併發性和吞吐量。有時候學習的最好方法是分析專家的成果,因此這個月咱們將分析 Doug Lea 的 util.concurrent 包中的 ConcurrentHashMap 的實現。JSR 133 將指定 ConcurrentHashMap 的一個版本,該版本針對 Java 內存模型(JMM)做了優化,它將包含在 JDK 1.5 的 java.util.concurrent 包中。util.concurrent 中的版本在老的和新的內存模型中都已經過線程安全審覈。 安全

針對吞吐量進行優化數據結構

ConcurrentHashMap 使用了幾個技巧來得到高程度的併發以及避免鎖定,包括爲不一樣的 hash bucket(桶)使用多個寫鎖和使用 JMM 的不肯定性來最小化鎖被保持的時間――或者根本避免獲取鎖。對於大多數通常用法來講它是通過優化的,這些用法每每會檢索一個極可能在 map 中已經存在的值。事實上,多數成功的 get() 操做根本不須要任何鎖定就能運行。(警告:不要本身試圖這樣作!想比 JMM 聰明不像看上去的那麼容易。util.concurrent 類是由併發專家編寫的,而且在 JMM 安全性方面通過了嚴格的同行評審。)併發

多個寫鎖app

咱們能夠回想一下,Hashtable(或者替代方案 Collections.synchronizedMap)的可伸縮性的主要障礙是它使用了一個 map 範圍(map-wide)的鎖,爲了保證插入、刪除或者檢索操做的完整性必須保持這樣一個鎖,並且有時候甚至還要爲了保證迭代遍歷操做的完整性保持這樣一個鎖。這樣一來,只要鎖被保持,就從根本上阻止了其餘線程訪問 Map,即便處理器有空閒也不能訪問,這樣大大地限制了併發性。ide

ConcurrentHashMap 摒棄了單一的 map 範圍的鎖,取而代之的是由 32 個鎖組成的集合,其中每一個鎖負責保護 hash bucket 的一個子集。鎖主要由變化性操做(put() remove())使用。具備 32 個獨立的鎖意味着最多能夠有 32 個線程能夠同時修改 map。這並不必定是說在併發地對 map 進行寫操做的線程數少於 32 時,另外的寫操做不會被阻塞――32 對於寫線程來講是理論上的併發限制數目,可是實際上可能達不到這個值。可是,32 依然比 1 要好得多,並且對於運行於目前這一代的計算機系統上的大多數應用程序來講已經足夠了。&#160

map 範圍的操做

有 32 個獨立的鎖,其中每一個鎖保護 hash bucket 的一個子集,這樣須要獨佔訪問 map 的操做就必須得到全部32個鎖。一些 map 範圍的操做,好比說size() isEmpty(),也許可以不用一次鎖整個 map(經過適當地限定這些操做的語義),可是有些操做,好比 map 重排(擴大 hash bucket 的數量,隨着 map 的增加從新分佈元素),則必須保證獨佔訪問。Java 語言不提供用於獲取可變大小的鎖集合的簡便方法。必須這麼作的狀況不多見,一旦碰到這種狀況,能夠用遞歸方法來實現。


JMM概述

在進入 put()get()remove() 的實現以前,讓咱們先簡單地看一下 JMM。JMM 掌管着一個線程對內存的動做 (讀和寫)影響其餘線程對內存的動做的方式。因爲使用處理器寄存器和預處理 cache 來提升內存訪問速度帶來的性能提高,Java 語言規範(JLS)容許一些內存操做並不對於全部其餘線程當即可見。有兩種語言機制可用於保證跨線程內存操做的一致性――synchronizedvolatile。

按照 JLS 的說法,「在沒有顯式同步的狀況下,一個實現能夠自由地更新主存,更新時所採起的順序多是出人意料的。」其意思是說,若是沒有同步的話,在一個給定線程中某種順序的寫操做對於另一個不一樣的線程來講可能呈現出不一樣的順序, 而且對內存變量的更新從一個線程傳播到另一個線程的時間是不可預測的。

雖然使用同步最多見的緣由是保證對代碼關鍵部分的原子訪問,但實際上同步提供三個獨立的功能――原子性、可見性和順序性。原子性很是簡單――同步實施一個可重入的(reentrant)互斥,防止多於一個的線程同時執行由一個給定的監視器保護的代碼塊。不幸的是,多數文章都只關注原子性方面,而忽略了其餘方面。可是同步在 JMM 中也扮演着很重要的角色,會引發 JVM 在得到和釋放監視器的時候執行內存壁壘(memory barrier)。

一個線程在得到一個監視器以後,它執行一個讀屏障(read barrier)――使得緩存在線程局部內存(好比說處理器緩存或者處理器寄存器)中的全部變量都失效,這樣就會致使處理器從新從主存中讀取同步代碼塊使用的變量。與此相似,在釋放監視器時,線程會執行一個寫屏障(write barrier)――將全部修改過的變量寫回主存。互斥獨佔和內存壁壘結合使用意味着只要您在程序設計的時候遵循正確的同步法則(也就是說,每當寫一個後面可能被其餘線程訪問的變量,或者讀取一個可能最後被另外一個線程修改的變量時,都要使用同步),每一個線程都會獲得它所使用的共享變量的正確的值。

若是在訪問共享變量的時候沒有同步的話,就會發生一些奇怪的事情。一些變化可能會經過線程當即反映出來,而其餘的則須要一些時間(這由關聯緩存的本質所致)。結果,若是沒有同步您就不能保證內存內容一定一致(相關的變量相互間可能會不一致),或者不能獲得當前的內存內容(一些值多是過期的)。避免這種危險狀況的經常使用方法(也是推薦使用的方法)固然是正確地使用同步。然而在有些狀況下,好比說在像 ConcurrentHashMap 之類的一些使用很是普遍的庫類中,在開發過程中還須要一些額外的專業技能和努力(可能比通常的開發要多出不少倍)來得到較高的性能。


ConcurrentHashMap 實現

如前所述,ConcurrentHashMap 使用的數據結構與 HashtableHashMap 的實現相似,是 hash bucket 的一個可變數組,每一個 ConcurrentHashMap 都由一個 Map.Entry 元素鏈構成,如清單1所示。與 HashtableHashMap 不一樣的是,ConcurrentHashMap 沒有使用單一的集合鎖(collection lock),而是使用了一個固定的鎖池,這個鎖池造成了bucket 集合的一個分區。


清單1. ConcurrentHashMap 使用的 Map.Entry 元素
protected static class Entry implements Map.Entry {
protected final Object key;
protected volatile Object value;
protected final int hash;
protected final Entry next;
...
}

不用鎖定遍歷數據結構

Hashtable 或者典型的鎖池 Map 實現不一樣,ConcurrentHashMap.get() 操做不必定須要獲取與相關bucket 相關聯的鎖。若是不使用鎖定,那麼實現必須有能力處理它用到的全部變量的過期的或者不一致的值,好比說列表頭指針和 Map.Entry 元素的域(包括組成每一個 hash bucket 條目的鏈表的連接指針)。

大多併發類使用同步來保證獨佔式訪問一個數據結構(以及保持數據結構的一致性)。ConcurrentHashMap 沒有采用獨佔性和一致性,它使用的鏈表是通過精心設計的,因此其實現能夠檢測到它的列表是否一致或者已通過時。若是它檢測到它的列表出現不一致或者過期,或者乾脆就找不到它要找的條目,它就會對適當的 bucket 鎖進行同步並再次搜索整個鏈。這樣作在通常的狀況下能夠優化查找,所謂的通常狀況是指大多數檢索操做是成功的而且檢索的次數多於插入和刪除的次數。

使用不變性

不一致性的一個重要來源是能夠避省得,其方法是使 Entry 元素接近不變性――除了值字段(它們是易變的)以外,全部字段都是 final 的。這就意味着不能將元素添加到 hash 鏈的中間或末尾,或者從 hash 鏈的中間或末尾刪除元素――而只能從 hash 鏈的開頭添加元素,而且刪除操做包括克隆整個鏈或鏈的一部分並更新列表的頭指針。因此說只要有對某個 hash 鏈的一個引用,即便可能不知道有沒有對列表頭節點的引用,您也能夠知道列表的其他部分的結構不會改變。並且,由於值字段是易變的,因此可以當即看到對值字段的更新,從而大大簡化了編寫可以處理內存潛在過期的 Map 的實現。

新的 JMM 爲 final 型變量提供初始化安全,而老的 JMM 不提供,這意味着另外一個線程看到的多是 final 字段的默認值,而不是對象的構造方法提供的值。實現必須可以同時檢測到這一點,這是經過保證 Entry中每一個字段的默認值不是有效值來實現的。這樣構造好列表以後,若是任何一個 Entry 字段有其默認值(零或空),搜索就會失敗,提示同步 get() 並再次遍歷鏈。

檢索操做

檢索操做首先爲目標 bucket 查找頭指針(是在不鎖定的狀況下完成的,因此說多是過期的),而後在不獲取 bucket 鎖的狀況下遍歷 bucket 鏈。若是它不能發現要查找的值,就會同步並試圖再次查找條目,如清單2 所示:


清單2. ConcurrentHashMap.get() 實現
public Object get(Object key) {
int hash = hash(key); // throws null pointer exception if key is null

// Try first without locking...
Entry[] tab = table;
int index = hash & (tab.length - 1);
Entry first = tab[index];
Entry e;

for (e = first; e != null; e = e.next) {
  if (e.hash == hash && eq(key, e.key)) {
Object value = e.value;
// null values means that the element has been removed
if (value != null) 
  return value;
else
  break;
  }
}

// Recheck under synch if key apparently not there or interference
Segment seg = segments[hash & SEGMENT_MASK];
synchronized(seg) { 
  tab = table;
  index = hash & (tab.length - 1);
  Entry newFirst = tab[index];
  if (e != null || first != newFirst) {
for (e = newFirst; e != null; e = e.next) {
  if (e.hash == hash && eq(key, e.key)) 
return e.value;
}
  }
  return null;
}
  }

刪除操做

由於一個線程可能看到 hash 鏈中連接指針的過期的值,簡單地從鏈中刪除一個元素不足以保證其餘線程在進行查找的時候不繼續看到被刪除的值。相反,從清單3咱們能夠看到,刪除操做分兩個過程――首先找到適當的 Entry 對象並把其值字段設爲 null,而後對鏈中從頭元素到要刪除的元素的部分進行克隆,再鏈接到要刪除的元素以後的部分。由於值字段是易變的,若是另一個線程正在過期的鏈中查找那個被刪除的元素,它會當即看到一個空值,並知道使用同步從新進行檢索。最終,原始 hash 鏈中被刪除的元素將會被垃圾收集。


清單3. ConcurrentHashMap.remove() 實現
protected Object remove(Object key, Object value) {
/*
  Find the entry, then 
1. Set value field to null, to force get() to retry
2. Rebuild the list without this entry.
   All entries following removed node can stay in list, but
   all preceding ones need to be cloned.  Traversals rely
   on this strategy to ensure that elements will not be
  repeated during iteration.
*/

int hash = hash(key);
Segment seg = segments[hash & SEGMENT_MASK];

synchronized(seg) {
  Entry[] tab = table;
  int index = hash & (tab.length-1);
  Entry first = tab[index];
  Entry e = first;

  for (;;) {
if (e == null)
  return null;
if (e.hash == hash && eq(key, e.key)) 
  break;
e = e.next;
  }

  Object oldValue = e.value;
  if (value != null && !value.equals(oldValue))
return null;
 
  e.value = null;

  Entry head = e.next;
  for (Entry p = first; p != e; p = p.next) 
head = new Entry(p.hash, p.key, p.value, head);
  tab[index] = head;
  seg.count--;
  return oldValue;
}
  }

圖1爲刪除一個元素以前的 hash 鏈:


圖1. Hash鏈
Figure 1. Hash chain

圖2爲刪除元素3以後的鏈:


圖2. 一個元素的刪除過程
Figure 2. Removal of an element

插入和更新操做

put() 的實現很簡單。像 remove() 同樣,put() 會在執行期間保持 bucket 鎖,可是因爲 put() 並非都須要獲取鎖,因此這並不必定會阻塞其餘讀線程的執行(也不會阻塞其餘寫線程訪問別的 bucket)。它首先會在適當的 hash 鏈中搜索須要的鍵值。若是可以找到,value字段(易變的)就直接被更新。若是沒有找到,新會建立一個用於描述新 map 的新 Entry 對象,而後插入到 bucket 列表的頭部。

弱一致的迭代器

ConcurrentHashMap 返回的迭代器的語義又不一樣於 ava.util 集合中的迭代器;並且它又是 弱一致的(weakly consistent) 而非 fail-fast 的(所謂 fail-fast 是指,當正在使用一個迭代器的時候,如何底層的集合被修改,就會拋出一個異常)。當一個用戶調用 keySet().iterator() 去迭代器中檢索一組 hash 鍵的時候,實現就簡單地使用同步來保證每一個鏈的頭指針是當前值。next()hasNext() 操做以一種明顯的方式定義,即遍歷每一個鏈而後轉到下一個鏈直到全部的鏈都被遍歷。弱一致迭代器可能會也可能不會反映迭代器迭代過程當中的插入操做,可是必定會反映迭代器尚未到達的鍵的更新或刪除操做,而且對任何值最多返回一次。ConcurrentHashMap 返回的迭代器不會拋出 ConcurrentModificationException 異常。

動態調整大小

隨着 map 中元素數目的增加,hash 鏈將會變長,所以檢索時間也會增長。從某種意義上說,增長 bucket 的數目和重排其中的值是很是重要的。在有些像 Hashtable 之類的類中,這很簡單,由於保持一個應用到整個 map 的獨佔鎖是可能的。在 ConcurrentHashMap 中,每次一個條目插入的時候,若是鏈的長度超過了某個閾值,鏈就被標記爲須要調整大小。當有足夠多的鏈被標記爲須要調整大小之後,ConcurrentHashMap 就使用遞歸獲取每一個 bucket 上的鎖並重排每一個 bucket 中的元素到一個新的、更大的 hash 表中。多數狀況下,這是自動發生的,而且對調用者透明。

不鎖定?

要說不用鎖定就能夠成功地完成 get() 操做彷佛有點言過其實,由於 Entryvalue 字段是易變的,這是用來檢測更新和刪除的。在機器級,易變的和同步的內容一般在最後會被翻譯成相同的緩存一致原語,因此這裏會有 一些 鎖定,雖然只是細粒度的而且沒有調度,或者沒有獲取和釋放監視器的 JVM 開銷。可是,除語義以外,在不少通用的狀況下,檢索的次數大於插入和刪除的次數,因此說由 ConcurrentHashMap 取得的併發性是至關高的。


結束語

ConcurrentHashMap 對於不少併發應用程序來講是一個很是有用的類,並且對於理解 JMM 何以取得較高性能的微妙細節是一個很好的例子。ConcurrentHashMap是編碼的經典,須要深入理解併發和 JMM 纔可以寫得出。使用它,從中學到東西,享受其中的樂趣――可是除非您是 Java 併發方面的專家,不然的話您本身不該該這樣試。

相關文章
相關標籤/搜索