ConcurrentHashMap的實現原理與使用


什么是ConcurrentHashMap?

ConcurrentHashMap 是java集合中map的實現,是哈希表的線程安全版本,即使是線程安全版本,

ConcurrentHashMap的性能也十分可觀。但是在不同的jdk版本中,其實現也不一樣,本文主要基於jdk1.8版本的實現討論。

ConcurrentHashMap 是線程安全且高效的HashMap。

 

為什么要使用ConcurrentHashMap?

1 線程不安全的HashMap(在多線程環境下,使用HashMap 進行put操作會引起死循環,導致CPU利用率接近100%)

2 效率低下的HashTable(HashTable 容器使用synchronized 來保證線程安全,但在線程競爭激烈的情況下HashTable 的效率非常低下)

3ConcurrentHashMap 的鎖 分 段技術可有效提升並發訪問率。

 

ConcurrentHashMap 結構

  ConcurrentHashMap 是由Segment 數組結構和HashEntry數組結構組成。

  Segment 是一種可重入鎖(ReentrantLock),在ConcurrentHashMap 里扮演鎖的角色。

  一個ConCurrentHashMap里包含一個 Segment數組。

 Segment的結構和HashMap 類似,是一種數組和鏈表結構。

 HashEntry 則用於存儲鍵值 對數據。

 

初始化

 ConcurrentHaspMap初始化方法是通過initialCapacity,loadFactor, concurrencyLevel幾個參數來初始化segments數組,段偏移量segmentShift,段掩碼segmentMask和每個segment里的HashEntry數組

 

為了能通過按位與的哈希算法來定位segments數組的索引,必須保證segments數組的長度是2的N次方(power-of-two size),所以必須計算出一個是大於或等於concurrencyLevel的最小的2的N次方值來作為segments數組的長度。假如concurrencyLevel等於14,15或16,ssize都會等於16,即容器里鎖的個數也是16

注意concurrencyLevel的最大大小是65535,意味着segments數組的長度最大為65536,對應的二進制是16位

 

ConcurrentHaspMap 操作

get操作(不加鎖)

    segment 的get 操作實現非常簡單和高效,先經過一次再散列,然后使用這個散列值通過散列運算定位到Segment 再通過散列算法定位到元素。

  思路:  (1)確定鍵值對在哪個段

                (2)確定鍵值對在哪個小的鏈表上 tab[index]

                 (3)遍歷鏈表,找到指定的key

get方法步驟:
1、計算key的hash值,並定位table索引
2、若table索引下元素(head節點)為普通鏈表,則按鏈表的形式迭代遍歷。
3、若table索引下元素為紅黑樹TreeBin節點,則按紅黑樹的方式查找(find方法)。

 

put操作 添加鍵值對.(加鎖)

由於put方法里需要對共享變量進行寫入操作,所以為了線程安全,在操作共享變量時必須加鎖。put方法首先定位到Segment,然后在Segment里進行插入操作

size操作(先嘗試不加鎖,再嘗試加鎖)

如果要統計整個ConcurrentHashMap里元素的大小,就必須統計所有Segment里元素的大小后求和。

 

實列:

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CountDownLatch;

public class MyConcurrentMashMapTest {



public static void main(String[] arg)
{
//ConcurrentHashMap 是java集合中map的實現,是哈希表的線程安全版本,即使是線程安全版本
Map<String,Integer> count=new ConcurrentHashMap<>();
//CountDownLatch其實可以把它看作一個計數器,只不過這個計數器的操作是原子操作,同時只能有一個線程去操作這個計數器,也就是同時只能有一個線程去減這個計數器里面的值
CountDownLatch endLatch = new CountDownLatch(3);

Runnable runnable=new Runnable() {
@Override
public void run() {
Integer oldvalue;
for(int i=0;i<5;i++)
{
//
Integer value = count.get("123");
if (null == value) {
// 添加鍵值對
count.put("123", 1);
} else {

count.put("123", value + 1);
}

}
//countDown 的時候每次調用都會對 state 減 1 也就是我們
// new CountDownLatch(3); 的這個計數器的數字減 1
endLatch.countDown();

}
};

//new CountDownLatch(3); 3 代表 3個線程
new Thread(runnable).start();
new Thread(runnable).start();
new Thread(runnable).start();

try {
//此方法用來讓當前線程阻塞,直到count減小為0才恢復執行,await 方法它會去獲取同步值發現為
// 0 的話成功返回,如果小於 0 的話,再次判斷是否是頭結點
endLatch.await();
System.out.println(count);
} catch (Exception e) {
e.printStackTrace();
}


}
}

結果:{123=15}

1. ConcurrentHashMap中變量使用final和volatile修飾有什么用呢?
Final域使得確保初始化安全性(initialization safety)成為可能,初始化安全性讓不可變形對象不需要同步就能自由地被訪問和共享。
使用volatile來保證某個變量內存的改變對其他線程即時可見,在配合CAS可以實現不加鎖對並發操作的支持。get操作可以無鎖是由於Node的元素val和指針next是用volatile修飾的,在多線程環境下線程A修改結點的val或者新增節點的時候是對線程B可見的。

2.我們可以使用CocurrentHashMap來代替Hashtable嗎?
我們知道Hashtable是synchronized的,但是ConcurrentHashMap同步性能更好,因為它僅僅根據同步級別對map的一部分進行上鎖。ConcurrentHashMap當然可以代替HashTable,但是HashTable提供更強的線程安全性。它們都可以用於多線程的環境,但是當Hashtable的大小增加到一定的時候,性能會急劇下降,因為迭代時需要被鎖定很長的時間。因為ConcurrentHashMap引入了分割(segmentation),不論它變得多么大,僅僅需要鎖定map的某個部分,而其它的線程不需要等到迭代完成才能訪問map。簡而言之,在迭代的過程中,ConcurrentHashMap僅僅鎖定map的某個部分,而Hashtable則會鎖定整個map。

3. ConcurrentHashMap有什么缺陷嗎?
ConcurrentHashMap 是設計為非阻塞的。在更新時會局部鎖住某部分數據,但不會把整個表都鎖住。同步讀取操作則是完全非阻塞的。好處是在保證合理的同步前提下,效率很高。壞處是嚴格來說讀取操作不能保證反映最近的更新。例如線程A調用putAll寫入大量數據,期間線程B調用get,則只能get到目前為止已經順利插入的部分數據。

4. ConcurrentHashMap在JDK 7和8之間的區別

  • JDK1.8的實現降低鎖的粒度,JDK1.7版本鎖的粒度是基於Segment的,包含多個HashEntry,而JDK1.8鎖的粒度就是HashEntry(首節點)
  • JDK1.8版本的數據結構變得更加簡單,使得操作也更加清晰流暢,因為已經使用synchronized來進行同步,所以不需要分段鎖的概念,也就不需要Segment這種數據結構了,由於粒度的降低,實現的復雜度也增加了
  • JDK1.8使用紅黑樹來優化鏈表,基於長度很長的鏈表的遍歷是一個很漫長的過程,而紅黑樹的遍歷效率是很快的,代替一定閾值的鏈表,這樣形成一個最佳拍檔

 

總結:

1.ConcurrentHashMap的數據結構與HashMap基本相同,只是在put的過程中如果沒有發生沖突,則采用CAS操作進行無鎖化更新,只有發生了哈希沖突的時候才鎖住在鏈表上添加新Node或者更新Node的操作。

2.像get一類的操作也是沒有同步的。

3.ConcurrentHashMap 不允許存放null值。

4.ConcurrentHashMap 的大小是通過計算出來的,也就是說在超高的並發情況下,size是不精確的。這一點后面有空再補上。

5.和jdk1.7 相比,在jdk1.7中采用鎖分段技術,更加復雜一點,jdk1.8中ConcurrentHashMap上鎖僅在發生hash沖突時才上鎖,且僅影響發生沖突的那一個鏈表的更新操作。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM