1. 程式人生 > 實用技巧 >ConcurrentHashMap的實現原理與使用

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衝突時才上鎖,且僅影響發生衝突的那一個連結串列的更新操作。