溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

使用Java如何實現(xiàn)ConcurrentHashMap#put并發(fā)容器

發(fā)布時間:2020-11-23 16:33:52 來源:億速云 閱讀:133 作者:Leah 欄目:編程語言

使用Java如何實現(xiàn)ConcurrentHashMap#put并發(fā)容器?針對這個問題,這篇文章詳細介紹了相對應(yīng)的分析和解答,希望可以幫助更多想解決這個問題的小伙伴找到更簡單易行的方法。

jdk1.7.0_79

HashMap可以說是每個Java程序員用的最多的數(shù)據(jù)結(jié)構(gòu)之一了,無處不見它的身影。關(guān)于HashMap,通常也能說出它不是線程安全的。這篇文章要提到的是在多線程并發(fā)環(huán)境下的HashMap——ConcurrentHashMap,顯然它必然是線程安全的,同樣我們不可避免的要討論散列表,以及它是如何實現(xiàn)線程安全的,它的效率又是怎樣的,因為對于映射容器還有一個Hashtable也是線程安全的但它似乎只出現(xiàn)在筆試、面試題里,在現(xiàn)實編碼中它已經(jīng)基本被遺棄。

關(guān)于HashMap的線程不安全,在多線程并發(fā)環(huán)境下它所帶來的影響絕不僅僅是出現(xiàn)臟數(shù)據(jù)等數(shù)據(jù)不一致的情況,嚴重的是它有可能帶來程序死循環(huán),這可能有點不可思議,但確實在不久前的項目里同事有遇到了CPU100%滿負荷運行,分析結(jié)果是在多線程環(huán)境下HashMap導(dǎo)致程序死循環(huán)。對于Hashtable,查看其源碼可知,Hashtable保證線程安全的方式就是利用synchronized關(guān)鍵字,這樣會導(dǎo)致效率低下,但對于ConcurrentHashMap則采用了不同的線程安全保證方式——分段鎖。它不像Hashtable那樣將整個table鎖住而是將數(shù)組元素分段加鎖,如果線程1訪問的元素在分段segment1,而線程2訪問的元素在分段segment2,則它們互不影響可以同時進行操作。如果合理的進行分段就是其關(guān)鍵問題。

ConcurrentHashMap和HashMap的結(jié)果基本一致,同樣也是Entry作為存放數(shù)據(jù)的對象,另外一個就是上面提到的分段鎖——Segment。它繼承自ReentrantLock(關(guān)于ReentrantLock,可參考《5.Lock接口及其實現(xiàn)ReentrantLock》),故它具有ReentrantLock一切特性——可重入,獨占等。

ConcurrentHashMap的結(jié)構(gòu)圖如下所示:

使用Java如何實現(xiàn)ConcurrentHashMap#put并發(fā)容器

可以看到相比較于HashMap,ConcurrentHashMap在Entry數(shù)組之上是Segment,這個就是我們上面提到的分段鎖,合理的確定分段數(shù)就能更好的提高并發(fā)效率,我們來看ConcurrentHashMap是如何確定分段數(shù)的。

ConcurrentHashMap的初始化時通過其構(gòu)造函數(shù)public ConcurrentHashMap(int initialCapacity, float loadFactor, int concurrencyLevel)完成的,若在不指定各參數(shù)的情況下,初始容量initialCapacity=DAFAULT_INITIAL_CAPACITY=16,負載因子loadFactor=DEFAULT_LOAD_FACTOR=0.75f,并發(fā)等級concurrencyLevel=DEFAULT_CONCURRENCY_LEVEL=16,前兩者和HashMap相同。至于負載因子表示一個散列表的空間的使用程度,initialCapacity(總?cè)萘? * loadFactor(負載因子) = 數(shù)據(jù)量,有此公式可知,若負載因子越大,則散列表的裝填程度越高,也就是能容納更多的元素,但這樣元素就多,鏈表就大,此時索引效率就會降低。若負載因子越小,則相反,索引效率就會高,換來的代價就是浪費的空間越多。并發(fā)等級它表示估計最多有多少個線程來共同修改這個Map,稍后可以看到它和segment數(shù)組相關(guān),segment數(shù)組的長度就是通過concurrencyLevel計算得出。

//以默認參數(shù)為例initalCapacity=16,loadFactor=0.75,concurrencyLevel=16 
public ConcurrentHashMap(int initalCapacity, float loadFactor, int concurrencyLevel) { 
  if (!(loadFactor > 0) || initialCapacity < 0 || concurrencyLevel <= 0) 
    throw new IllegalArgumentException(); 
  if (concurrencyLevel > MAX_SEGMENTS) 
    concurrencyLevel = MAX_SEGMENTS; 
  int sshift = 0; 
  int ssize = 1;//segment數(shù)組長度 
  while (ssize < concurrencyLevel) { 
    ++sshift; 
    ssize <= 1; 
  }//經(jīng)過ssize左移4位后,ssize=16,ssift=4 
/*segmentShift用于參與散列運算的位數(shù),segmentMask是散列運算的掩碼,這里有關(guān)的散列函數(shù)運算和HashMap有類似之處*/ 
  this.segmentShift = 32 – ssift;//段偏移量segmentShift=28 
  this.segmentMask = ssize – 1;//段掩碼segmentMask=15(1111) 
  if (initialCapacity > MAXIMUM_CAPACITY) 
    initialCapacity = MAXIMUM_CAPACITY; 
  int c = initialCapacity / ssize;//c = 1 
  if (c * ssize < initialCapacity) 
    ++c; 
  int cap = MIN_SEGMENT_TABLE_CAPACITY;//MIN_SEGMENT_TABLE_CAPACITY=2 
  while (cap < c)//cap = 2, c = 1,false 
   cap <<= 1;//cap是segment里HashEntry數(shù)組的長度,最小為2 
/*創(chuàng)建segments數(shù)組和segment[0]*/ 
  Segment<K,V> s0 = new Segment<K,V>(loadFactor, (int)(cap * loadFactor), (HashEntry<K,V>[]) new HashEntry[cap]);//參數(shù)意為:負載因子=1,數(shù)據(jù)容量=(int)(2 * 0.75)=1,總?cè)萘?2,故每個Segment的HashEntry總?cè)萘繛?,實際數(shù)據(jù)容量為1 
  Segment<K,V> ss = (Segment<K,V>[])new Segment[ssize];//segments數(shù)組大小為16 
  UNSAFE.putOrderedObject(ss, SBASE, s0); 
  this.segments = ss; 
}

以上就是整個初始化過程,主要是初始化segments的長度大小以及通過負載因子確定每個Segment的容量大小。確定好Segment過后,接下來的重點就是如何準確定位Segment。定位Segment的方法就是通過散列函數(shù)來定位,先通過hash方法對元素進行二次散列,這個算法較為復(fù)雜,其目的只有一個——減少散列沖突,使元素能均勻分布在不同的Segment上,提高容器的存取效率。

我們通過最直觀最常用的put方法來觀察ConcurrentHashMap是如何通過key值計算hash值在定位到Segment的:

//ConcurrentHashMap#put 
public V put(K key, V value) { 
  Segment<K,V> s; 
  if (value == null) 
    throw new NullPointerException(); 
  int hash = hash(key);//根據(jù)散列函數(shù),計算出key值的散列值 
  int j = (hash >>> segmentShift) & segmentMask;//這個操作就是定位Segment的數(shù)組下標,jdk1.7之前是segmentFor返回Segment,1.7之后直接就取消了這個方法,直接計算數(shù)組下標,然后通過偏移量底層操作獲取Segment 
  if ((s = (Segment<K,V>)UNSAFE.getObject   // nonvolatile; recheck 
      (segments, (j << SSHIFT) + SBASE)) == null) // in ensureSegment 
    s = ensureSegment(j);//通過便宜量定位不到就調(diào)用ensureSegment方法定位Segment 
  return s.put(key, hash, value, false); 
}

Segment.put方法就是將鍵、值構(gòu)造為Entry節(jié)點加入到對應(yīng)的Segment段里,如果段中已經(jīng)有元素(即表示兩個key鍵值的hash值重復(fù))則將最新加入的放到鏈表的頭),整個過程必然是加鎖安全的。

不妨繼續(xù)深入Segment.put方法:

//Segment#put 
final V put(K key, int hash, V value, boolean onlyIfAbsent) { 
  HashEntry<K,V> node = tryLock() &#63; null : scanAndLockForPut(key, hash, value);//非阻塞獲取鎖,獲取成功node=null,失敗 
  V oldValue; 
  try { 
    HashEntry<K,V>[] tab = table;//Segment對應(yīng)的HashEntry數(shù)組長度 
    int index = (tab.length - 1) & hash; 
    HashEntry<K,V> first = entryAt(tab, index);//獲取HashEntry數(shù)組的第一個值 
    for (HashEntry<K,V> e = first;;) { 
      if (e != null) {//HashEntry數(shù)組已經(jīng)存在值 
        K k; 
        if ((k = e.key) == key || (e.hash == hash && key.equals(k))) {//key值和hash值都相等,則直接替換舊值 
          oldValue = e.value; 
          if (!onlyIfAbsent) { 
            e.value = value; 
            ++modCount; 
          } 
          break; 
        } 
        e = e.next;//不是同一個值則繼續(xù)遍歷,直到找到相等的key值或者為null的HashEntry數(shù)組元素 
      } 
      else {//HashEntry數(shù)組中的某個位置元素為null 
        if (node != null) 
          node.setNext(first);//將新加入節(jié)點(key)的next引用指向HashEntry數(shù)組第一個元素 
        else//已經(jīng)獲取到了Segment鎖 
          node = new HashEntry<K,V>(hash, key, value, first) 
        int c = count + 1; 
        if (c > threshold && tab.lenth < MAXIUM_CAPACITY)//插入前先判斷是否擴容,ConcurrentHashMap擴容與HashMap不同,ConcurrentHashMap只擴Segment的容量,HashMap則是整個擴容 
          rehash(node); 
        else 
          setEntryAt(tab, index, node);//設(shè)置為頭節(jié)點 
        ++modCount;//總?cè)萘?
        count = c; 
        oldValue = null; 
        break; 
      } 
     } 
  } finally { 
    unlock(); 
  } 
  return oldValue; 
}

上面大致就是ConcurrentHashMap加入一個元素的過程,需要明白的就是ConcurrentHashMap分段鎖的概念。在JDK1.6中定位Segment較為簡單,直接計算出Segment數(shù)組下標后就返回具體的Segment,而JDK1.7則通過偏移量來計算,算出為空時,還有一次檢查獲取Segment,猜測是1.7使用底層native是為了提高效率,JDK1.8的ConcurrentHashMap又有不同,暫未深入研究,它的數(shù)據(jù)結(jié)果似乎變成了紅黑樹。

有關(guān)ConcurrentHashMap的get方法不再分析,過程總結(jié)為一句話:根據(jù)key值計算出hash值,根據(jù)hash值計算出對應(yīng)的Segment,再在Segment下的HashEntry鏈表遍歷查找。

關(guān)于使用Java如何實現(xiàn)ConcurrentHashMap#put并發(fā)容器問題的解答就分享到這里了,希望以上內(nèi)容可以對大家有一定的幫助,如果你還有很多疑惑沒有解開,可以關(guān)注億速云行業(yè)資訊頻道了解更多相關(guān)知識。

向AI問一下細節(jié)

免責聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI