Java併發之ConcurrentHashMap

541732025發表於2014-04-01

ConcurrentHashMap,執行緒安全的HashMap,由於HashTable較重量級,他會給整個加鎖,而ConcurrentHashMap只是給每個Segment加鎖,所以效能快很多。
除了initialCapacity、loadFactor之外,還有一個concurrentLevel屬性,預設情況下,三個屬性分別為16,0.75,16
設定以上三個屬性後,就得考慮鎖加在哪?並怎樣初始化加鎖的物件?

點選(此處)摺疊或開啟

  1. int sshift = 0;
  2. int ssize = 1;
  3. while(ssize < concurrentLevel){
  4.     ++sshift;
  5.     ssize <<= 1;
  6. }

上面這段程式碼意思是:計算出一個不小於concurrentLevel的ssize值,而且它是2的n次方。
預設情況下,ssize為16,根據這個引數傳入Segment的newArray方法,建立大小為16的Segment陣列
建立Segment陣列後,陣列元素物件怎麼初始化?

點選(此處)摺疊或開啟

  1. int c = initialCapacity /ssize
  2. if(c* ssize < initialCapacity){
  3.     ++c;
  4. }
  5. int cap = 1;
  6. while(cap < c){
  7.     cap << 1;
  8. }

上面程式碼意思是:用Map容量除以Segment陣列大小,看每個Segment需要初始化多大,這裡16/16=1,所以建立大小為cap=1的HashEntry[]陣列,將其賦給Segment,並且基於cap值和loadFactor計算threshold值。Segment繼承自ReentrantLock。可以發現。一個Segment的資料結構就相當於HashMap(陣列下有連結串列

點選(此處)摺疊或開啟

  1. threshold = (int)(newTable.length * loadFactor)


put(key,value)
ConcurrentHashMap並沒有對整個方法加鎖(而HashTable對整個加鎖),和HashMap一樣,首先對key.hashCode進行hash操作,得到hash值後計算其對應在陣列中的哪個Segment物件。

點選(此處)摺疊或開啟

  1. return segments[(hash >>> segmentShift) & segmentMask]

找到陣列中的Segment物件後,接著呼叫Segment的put方法完成操作,至此,才對其進行加鎖:lock,接著判斷當前儲存的物件個數加1後是否大於threshold,如大於,則rehash,將當前HashEntry[]陣列擴大2倍,並重hash物件。
其餘的操作跟HashMap差不多,有則覆蓋,沒有則新建立HashEntry物件,放在連結串列頭部。

點選(此處)摺疊或開啟

  1. HashEntry<K,V>[] newTable = HashEntry.newArray(oldCapacity<<1)


get(key)
get操作只有在e.value == null的情況下,才會加lock再執行一次e.value

問題:get操作大部分情況沒有lock,它是怎樣保證併發下資料的一致性的呢?
譬如1:在get找HashEntry連結串列過程中,這時候可能HashEntry[]陣列會發生改變(put操作執行),那它是如何讓保證的呢?
答案就是因為HashEntry[]陣列是volatile的,當put改變陣列後,get操作會立刻得到更新。並且,jdk5以後,volatile語義增強了,不僅僅保證資料的可見性,還能保證禁止在物件上的讀寫重排序,所以,在get時讀取到的HashEntry[]是最新的、並且構造已經完全的
譬如2:當get操作已經找到了HashEntry,準備開始遍歷連結串列了,這時HashEntry發生變化了怎麼辦?
答案就是HashEntry物件中的hash、key、next屬性都是final的,這就意味著不能插入一個新的HashEntry在所遍歷的任何HashEntry的next下,這樣就可以保證當獲取到HashEntry物件後,其基於next屬性構建的連結串列是不會發生變化的。
還有一個問題,為什麼要判斷e.value是否為null?而且如果為null再呼叫readValueUnderLock(HashEntry e)?
以下為readValueUnderLock方法:

點選(此處)摺疊或開啟

  1. /**
  2.          * Reads value field of an entry under lock. Called if value
  3.          * field ever appears to be null. This is possible only if a
  4.          * compiler happens to reorder a HashEntry initialization with
  5.          * its table assignment, which is legal under memory model
  6.          * but is not known to ever occur.
  7.          */
  8.         V readValueUnderLock(HashEntry<K,V> e) {
  9.             lock();
  10.             try {
  11.                 return e.value;
  12.             } finally {
  13.                 unlock();
  14.             }
  15.         }



透過它的註釋,我們明白了,This is possible only if a compiler happens to reorder a HashEntry initialization with its table assignment,意思就是,只有在HashEntry初始化時出現指令重排,才會導致該方法呼叫,並且也不確定是否發生。

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/28912557/viewspace-1133899/,如需轉載,請註明出處,否則將追究法律責任。

相關文章