史上最詳細的ConcurrentHashMap詳解--源碼分析__源碼

來源:互聯網
上載者:User

ps.本文所有源碼都是基於jdk1.6
首先說明一點,ConcurrentHashMap並不是可以完全替換Hashtable的,因為ConcurrentHashMap的get、clear函數是弱一致的(後面會說到),而Hashtable是強一致的。有作者是這麼解釋的:我們將“一致性強度”和“擴充性”之間的對比交給使用者來權衡,所以大多數集合都提供了synchronized和concurrent兩個版本。不過真正需要“強一致性”的情境可能非常少,我們大多應用中ConcurrentHashMap是滿足的。 ConcurrentHashMap的資料結構

不得不說,ConcurrentHashMap設計的相當巧妙,它有多個段,每個段下面都是一個Hashtable(相似),所以每個段上都有一把鎖(分布式鎖),各個段之間的鎖互不影響,可以實現並行作業。話不多說,上代碼。

final Segment<K,V>[] segments;

可以看到ConcurrentHashMap實際上就是一個Segment數組,那麼Segment是什麼呢。

static final class Segment<K,V> extends ReentrantLock implements Serializable {    ...    transient volatile HashEntry<K,V>[] table; //發現Segment實際上是HashEntry數組    ...}

那HashEntry又是什麼呢。

static final class HashEntry<K,V> {    final K key;    final int hash;    volatile V value;    final HashEntry<K,V> next;    ...}

HashEntry是一個單鏈表
所以ConcurrentHashMap的資料結構如下圖:

這裡每一個segment所指向的資料結構,其實就是一個Hashtable,所以說每一個segment都有一把獨立的鎖,來保證當訪問不同的segment時互不影響。 ConcurrentHashMap的基礎方法

基礎方法分為這麼幾種:
1、段內加鎖的:put,putIfAbsent,remove,replace等
2、不加鎖的:get,containsKey等
3、整個資料結構加鎖的:size,containsValue等 建構函式

public ConcurrentHashMap(int initialCapacity,float loadFactor, int concurrencyLevel) {    if (!(loadFactor > 0) || initialCapacity < 0 || concurrencyLevel <= 0)        throw new IllegalArgumentException();  //參數合法性校正    if (concurrencyLevel > MAX_SEGMENTS)        concurrencyLevel = MAX_SEGMENTS;     //比如我輸入的concurrencyLevel=12,那麼sshift = 4,ssize =16,所以sshift是意思就是1左移了幾次比concurrencyLevel大,ssize就是那個大於等於concurrencyLevel的最小2的冪次方的數    int sshift = 0;    int ssize = 1;    while (ssize < concurrencyLevel) {        ++sshift;        ssize <<= 1;  //ssize = ssize << 1 , ssize = ssize * 2    }    segmentShift = 32 - sshift;    segmentMask = ssize - 1;   //segmentMask的二進位是一個全是1的數     this.segments = Segment.newArray(ssize); //segment個數是ssize,也就是上圖黃色方塊數,預設16個    if (initialCapacity > MAXIMUM_CAPACITY)        initialCapacity = MAXIMUM_CAPACITY;    int c = initialCapacity / ssize;    if (c * ssize < initialCapacity)        ++c;    int cap = 1;    while (cap < c)        cap <<= 1;       for (int i = 0; i < this.segments.length; ++i)        this.segments[i] = new Segment<K,V>(cap, loadFactor);//上圖淺綠色塊的個數是cap,是一個2的冪次方的數,預設是1,也就是每個segment下都構造了cap大小的table數組}Segment(int initialCapacity, float lf) {    loadFactor = lf;    setTable(HashEntry.<K,V>newArray(initialCapacity));//構造了一個initialCapacity大小的table}
put函數
public V put(K key, V value) {    if (value == null)        throw new NullPointerException(); //明確指定value不能為null    int hash = hash(key.hashCode());    return segmentFor(hash).put(key, hash, value, false); //segmentFor如下,定位segment下標,然後再put}final Segment<K,V> segmentFor(int hash) {         //尋找segment的下標    return segments[(hash >>> segmentShift) & segmentMask];     //前面說了segmentMask是一個2進位全是1的數,那麼&segmentMask就保證了下標小於等於segmentMask,與HashMap的尋下標相似。}//真正的put操作V put(K key, int hash, V value, boolean onlyIfAbsent) {    lock();          //先加鎖,可以看到,put操作是在segment裡面加鎖的,也就是每個segment都可以加一把鎖    try {        int c = count;        if (c++ > threshold) // ensure capacity            rehash();   //判斷容量,如果不夠了就擴容        HashEntry<K,V>[] tab = table;      //將table賦給一個局部變數tab,這是因為table是volatile變數,讀寫volatile變數的開銷很大,編譯器也不能對volatile變數的讀寫做任何最佳化,直接多次訪問非volatile執行個體變數沒有多大影響,編譯器會做相應最佳化。        int index = hash & (tab.length - 1);   //尋找table的下標        HashEntry<K,V> first = tab[index];        HashEntry<K,V> e = first;        while (e != null && (e.hash != hash || !key.equals(e.key)))            e = e.next;  //遍曆單鏈表,找到key相同的為止,如果沒找到,e指向鏈表尾        V oldValue;        if (e != null) { //如果有相同的key,那麼直接替換            oldValue = e.value;            if (!onlyIfAbsent)                e.value = value;        }        else {  //否則在鏈表表頭插入新的結點            oldValue = null;            ++modCount;            tab[index] = new HashEntry<K,V>(key, hash, first, value);            count = c; // write-volatile        }        return oldValue;    } finally {        unlock();    }}

該方法也是在持有段鎖的情況下執行的,首先判斷是否需要rehash,需要就先rehash,擴容都是針對單個段的,也就是單個段的資料數量大於設定的量的時候會觸發擴容。接著是找是否存在同樣一個key的結點,如果存在就直接替換這個結點的值。否則建立一個新的結點並添加到hash鏈的頭部,這時一定要修改modCount和count的值,同樣修改count的值一定要放在最後一步。put方法調用了rehash方法,reash方法實現得也很精巧,主要利用了table的大小為2^n,和HashMap的擴容基本一樣,這裡就不介紹了。

還有一個叫putIfAbsent(K key, V value)的函數,這個函數的實現和put幾乎一模一樣,作用是,如果map中不存在這個key,那麼插入這個資料,如果存在這個key,那麼不覆蓋原來的value,也就是不插入了。 remove函數

>remove操作也交給了段內的remove,如下:V remove(Object key, int hash, Object value) {    lock();  //段內先獲得一把鎖    try {        int c = count - 1;        HashEntry<K,V>[] tab = table;        int index = hash & (tab.length - 1);        HashEntry<K,V> first = tab[index];        HashEntry<K,V> e = first;        while (e != null && (e.hash != hash || !key.equals(e.key)))            e = e.next;        V oldValue = null;        if (e != null) {  //如果找到該key            V v = e.value;            if (value == null || value.equals(v)) {                oldValue = v;                // All entries following removed node can stay                // in list, but all preceding ones need to be                // cloned.                ++modCount;                HashEntry<K,V> newFirst = e.next; //newFirst此時為要刪除結點的next                for (HashEntry<K,V> p = first; p != e; p = p.next)                    newFirst = new HashEntry<K,V>(p.key,p.hash,newFirst, p.value);//從頭遍曆鏈表將要刪除結點的前面所有結點複製一份插入到newFirst之前,如下圖                tab[index] = newFirst;                count = c; // write-volatile            }        }        return oldValue;    } finally {        unlock();    }}

為什麼用這麼方式刪除呢,細心的同學會發現上面定義的HashEntry的key和next都是final類型的,所以不能改變next的指向,所以又複製了一份指向刪除的結點的next。 get函數

public V get(Object key) {    int hash = hash(key.hashCode());   //雙hash,和HashMap一樣,也是為了更好的離散化    return segmentFor(hash).get(key, hash);  //先尋找segment的下標,然後再get}final Segment<K,V> segmentFor(int hash) {      //尋找segment的下標    return segments[(hash >>> segmentShift) & segmentMask];  //前面說了segmentMask是一個2進位全是1的數,那麼&segmentMask就保證了下標小於等於segmentMask,與HashMap的尋下標相似。}V get(Object key, int hash) {// count是一個volatile變數,count非常巧妙,每次put和remove之後的最後一步都要更新count,就是為了get的時候不讓編譯器對代碼進行重排序,來保證    if (count != 0) {         HashEntry<K,V> e = getFirst(hash);   //尋找table的下標,也就是鏈表的表頭        while (e != null) {            if (e.hash == hash && key.equals(e.key)) {                V v = e.value;                if (v != null)                    return v;                return readValueUnderLock(e); // recheck  加鎖讀,這個加鎖讀不用重新計算位置,而是直接拿e的值            }            e = e.next;        }    }    return null;}HashEntry<K,V> getFirst(int hash) {    HashEntry<K,V>[] tab = table;    return tab[hash & (tab.length - 1)];}

讀完上面代碼我有一個疑問,就是如果找到的key對應的value是null的話,加鎖再讀一次,既然上面put操作不允許value是null,那讀到的value為什麼會有null的情況呢。我們分析一下這種情況,就是put操作和get操作同時進行的時候,可以分為兩種情況:
1、put的key已經存在,由於value不是final的,可以直接更新,且value是volatile的,所以修改會立馬對get線程可見,而不用等到put方法結束。
2、put的key不存在,那麼將在鏈表表頭插入一個資料,那麼將new HashEntry賦值給tab[index]是否能立刻對執行get的線程可見呢,我們知道每次put完之後都要更新一個count變數(寫),而每次get資料的時候,再最一開始都要讀一個count變數(讀),而且發現這個count是volatile的,而對同一個volatile變數,有volatile寫 happens-before volatile讀,所以如果寫發生在讀之前,那麼new HashEntry賦值給tab[index]是對get線程可見的,但是如果寫沒有發生在讀之前,就無法保證new HashEntry賦值給tab[index]要先於get函數的getFirst(hash),也就是說,如果某個Segment執行個體中的put將一個Entry加入到了table中,在未執行count賦值操作之前有另一個線程執行了同一個Segment執行個體中的get,來擷取這個剛加入的Entry中的value,那麼是有可能取不到的,這也就是get的弱一致性。但是什麼時候會找到key但是讀到的value是null呢,仔細看下put操作的語句:tab[index] = new HashEntry(key, hash, first, value),在這條語句中,HashEntry建構函式中對value的賦值以及對tab[index]的賦值可能被重新排序,舉個例子就是這條語句有可能先執行對key賦值,再執行對tab[index]的賦值,最後對value賦值,如果在對tab和key都賦值但是對value還沒賦值的情況下的get就是一個空值。
詳細可以看看這篇文章:http://ifeve.com/concurrenthashmap-weakly-consistent/
這也就是說無鎖的get操作是一個弱一致性的操作。 clear函數

public void clear() {    for (int i = 0; i < segments.length; ++i)    //迴圈clear掉每個段中的內容        segments[i].clear();}void clear() {    if (count != 0) {        lock();    //段內加鎖        try {            HashEntry<K,V>[] tab = table;            for (int i = 0; i < tab.length ; i++)                tab[i] = null;            ++modCount;            count = 0; // write-volatile        } finally {            unlock();        }    }}

因為沒有全域的鎖,在清除完一個segments之後,正在清理下一個segments的時候,已經清理segments可能又被加入了資料,因此clear返回的時候,ConcurrentHashMap中是可能存在資料的。因此,clear方法是弱一致的。 size函數

public int size() {    final Segment<K,V>[] segments = this.segments;    long sum = 0;    long check = 0;    int[] mc = new int[segments.length];    // Try a few times to get accurate count. On failure due to    // continuous async changes in table, resort to locking.    for (int k = 0; k < RETRIES_BEFORE_LOCK; ++k) {        check = 0;        sum = 0;        int mcsum = 0;        for (int i = 0; i < segments.length; ++i) {            sum += segments[i].count;           //迴圈相加每個段內資料的個數            mcsum += mc[i] = segments[i].modCount;        //迴圈相加每個段內的modCount        }        if (mcsum != 0) {                   //如果是0,代表根本沒有過資料更改,也就是size是0            for (int i = 0; i < segments.length; ++i) {                check += segments[i].count;    //再次迴圈相加每個段內資料的個數,這裡為什麼會再算一次呢,後面會說                if (mc[i] != segments[i].modCount) {                        check = -1; // force retry  //如果modCount和之前統計的不一致了,check直接賦值-1,重新再來                    break;                }            }        }        if (check == sum)            break;    }    if (check != sum) { // Resort to locking all segments        sum = 0;        for (int i = 0; i < segments.length; ++i)  //迴圈擷取所有segment的鎖            segments[i].lock();        for (int i = 0; i < segments.length; ++i)  //在持有所有段的鎖的時候進行count的相加            sum += segments[i].count;        for (int i = 0; i < segments.length; ++i)  //迴圈釋放所有段的鎖            segments[i].unlock();    }    if (sum > Integer.MAX_VALUE)            //return        return Integer.MAX_VALUE;    else        return (int)sum;}

如果我們要統計整個ConcurrentHashMap裡元素的大小,就必須統計所有Segment裡元素的大小後求和。Segment裡的全域變數count是一個volatile變數,那麼在多線程情境下,我們是不是直接把所有Segment的count相加就可以得到整個ConcurrentHashMap大小了呢。不是的,雖然相加時可以擷取每個Segment的count的最新值,但是拿到之後可能累加前使用的count發生了變化,那麼統計結果就不準了。所以最安全的做法,是在統計size的時候把所有Segment的put,remove和clean方法全部鎖住,但是這種做法顯然非常低效,因為在累加count操作過程中,之前累加過的count發生變化的幾率非常小,所以ConcurrentHashMap的做法是先嘗試2次通過不鎖住Segment的方式來統計各個Segment大小,如果統計的過程中,容器的count發生了變化,則再採用加鎖的方式來統計所有Segment的大小。那麼ConcurrentHashMap是如何判斷在統計的時候容器是否發生了變化呢。使用modCount變數,在put , remove和clear方法裡操作元素前都會將變數modCount進行加1,那麼在統計size前後比較modCount是否發生變化,從而得知容器的大小是否發生變化。size()的實現還有一點需要注意,必須要先segments[i].count,才能segments[i].modCount,這是因為segment[i].count是對volatile變數的訪問,接下來segments[i].modCount才能得到幾乎最新的值,這裡和get方法的方式是一樣的,也是一個volatile寫 happens-before volatile讀的問題。

上面18行代碼,拋出了一個問題,就是為什麼會再算一遍,上面說只需要比較modCount不變不就可以了麼。但是仔細分析,就會發現,在13行14行代碼那裡,計算完count之後,計算modCount之前有可能count的值又變了,那麼18行的代碼主要是解決這個問題。 containsValue函數

containsKey函數比較簡單,也是不加鎖的簡易get,下面說一下containsValue有一個有意思的地方

public boolean containsValue(Object value) {    if (value == null)        throw new NullPointerException();    // See explanation of modCount use above    final Segment<K,V>[] segments = this.segments;    int[] mc = new int[segments.length];    // Try a few times without locking    for (int k = 0; k < RETRIES_BEFORE_LOCK; ++k) {        int sum = 0;        int mcsum = 0;        for (int i = 0; i < segments.length; ++i) {            int c = segments[i].count;    //注意這行,聲明了一個c變數,並且賦值了,但是下面卻完全沒有用到。            mcsum += mc[i] = segments[i].modCount;            if (segments[i].containsValue(value))                return true;        }        boolean cleanSweep = true;    //預設結構沒變        if (mcsum != 0) {            for (int i = 0; i < segments.length; ++i) {                int c = segments[i].count;                if (mc[i] != segments[i].modCount) {  //如果結構變了,cleanSweep = false                    cleanSweep = false;                    break;                }            }        }        if (cleanSweep)    //如果沒變,直接返回false            return false;    }    // Resort to locking all segments    for (int i = 0; i < segments.length; ++i)        segments[i].lock();    boolean found = false;    try {        for (int i = 0; i < segments.length; ++i) {            if (segments[i].containsValue(value)) {                found = true;                break;            }        }    } finally {        for (int i = 0; i < segments.length; ++i)            segments[i].unlock();    }    return found;}

注意上面代碼15行處,裡面有語句int c = segments[i].count; 但是c卻從來沒有被使用過,即使如此,編譯器也不能做最佳化將這條語句去掉,因為存在對volatile變數count的讀取,這條語句存在的唯一目的就是保證segments[i].modCount讀取到幾乎最新的值,上面有說道這個問題。

本文章參考文章:
1、http://ifeve.com/concurrenthashmap-weakly-consistent/
2、http://www.iteye.com/topic/344876

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.