ConcurrentHashMap源碼刨析(基於jdk1.7)

看源碼前咱們必須先知道一下ConcurrentHashMap的基本結構。ConcurrentHashMap是採用分段鎖來進行併發控制的。node

其中有一個內部類爲Segment類用來表示鎖。而Segment類裏又有一個HashEntry<K,V>[]數組,這個數組纔是真正用算法

來存放咱們的key-value的。數組

大概爲以下圖結構。一個Segment數組,而Segment數組每一個元素爲一個HashEntry數組安全

 

看源碼前咱們還必須瞭解的幾個默認的常量值:併發

  • DEFAULT_INITIAL_CAPACITY = 16   容器默認容量爲16
  • DEFAULT_LOAD_FACTOR = 0.75f     默認擴容因子是0.75
  • DEFAULT_CONCURRENCY_LEVEL = 16  默認併發度是16
  • MAXIMUM_CAPACITY = 1 << 30      容器最大容量爲1073741824
  • MIN_SEGMENT_TABLE_CAPACITY = 2  段的最小大小
  • MAX_SEGMENTS = 1 << 16          段的最大大小
  • RETRIES_BEFORE_LOCK = 2         經過不獲取鎖的方式嘗試獲取size的次數

 

以上以及默認值是ConcurrentHashMap中定義好的,下面咱們不少地方會用到他們。ssh

先從初始化開始提及

經過咱們使用ConcurrentHashMap都是經過 ConcurrentHashMap<String,String> map = new ConcurrentHashMap<>();的方式函數

咱們點進去跟蹤下源碼this

/**
     * Creates a new, empty map with a default initial capacity (16),
     * load factor (0.75) and concurrencyLevel (16).
     */
    public ConcurrentHashMap() {
        this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR, DEFAULT_CONCURRENCY_LEVEL);
    }

能夠看到,默認無參構造函數內調用了另外一個帶參構造函數,而這個構造函數也就是無論你初始化時傳進來什麼參數,最終都會跳到那個帶參構造函數。spa

點進去看看這個帶參構造函數實現了什麼功能線程

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;
        // Find power-of-two sizes best matching arguments
        int sshift = 0;
        int ssize = 1;
        while (ssize < concurrencyLevel) {
            ++sshift;
            ssize <<= 1;
        }
        this.segmentShift = 32 - sshift;
        this.segmentMask = ssize - 1;
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
        int c = initialCapacity / ssize;
        if (c * ssize < initialCapacity)
            ++c;
        int cap = MIN_SEGMENT_TABLE_CAPACITY;
        while (cap < c)
            cap <<= 1;
        // create segments and segments[0]
        Segment<K,V> s0 =
            new Segment<K,V>(loadFactor, (int)(cap * loadFactor),
                             (HashEntry<K,V>[])new HashEntry[cap]);
        Segment<K,V>[] ss = (Segment<K,V>[])new Segment[ssize];
        UNSAFE.putOrderedObject(ss, SBASE, s0); // ordered write of segments[0]
        this.segments = ss;
    }

咱們看到該構造函數一共有三個參數,分別是容器的初始化大小、負載因子、併發度,這三個參數若是咱們new 一個ConcurrentHashMap時沒有指定,

那麼將會採用默認的參數,也就是咱們本文開始說的那幾個常量值。

在這裏我對這三個參數作下解釋。容器初始化大小是整個map的容量。負載因子是用來計算每一個segment裏的HashEntry數組擴容時的閾值的。併發度是

用來設置segment數組的長度的。

開頭這兩個if沒什麼好說的。就是用來判斷咱們傳進來的參數的正確性。當負載因子,初始容量和併發度不按照規範來時會拋出算術異常。第二個if時當傳進來的

併發度大於最大段大小的時候,就將其設置爲最大段大小。

這段就比較有意思了。因爲segment數組要求長度必須爲2的n次方,當咱們傳進來的併發度不是2的n次方時會計算出一個最接近它的2的n次方值

好比如何咱們傳進來的併發度爲14 15那麼經過計算segment數組長度就是16。在上圖中咱們能夠看到兩個局部變量ssize和sshift,在循環中若是ssize小於

併發度就將其二進制左移一位,即乘2。所以ssize就是用來保存咱們計算出來的最接近併發度的2的n次方值。而ssfhit是用來計算偏移量的。在這裏咱們又

要說兩個很重要的全局常量。segmentMask和segmentShift。其中segmentMask爲ssize - 1,因爲ssize爲2的倍數。那麼segmentMask就是奇數。化爲

二進制就是全1,而segmentShift爲32 - sshift大小。32是key值通過再hash求出來的值的二進制位。segmentMask和segmentShift是用來定位當前元素

在segment數組那個位置,和在HashEntry數組的哪一個位置,後面咱們會詳細說說怎麼算的。

這一段代碼就是用來肯定每一個segment裏面的hashentry的一些參數和初始化segment數組了。第一個if是防止咱們設置的初始化

容量大於最大容量。而c是用來計算每一個hashentry數組的容量。因爲每一個hashentry數組容量也須要爲2的n次方,所以這裏也須要

一個cap和循環來計算一個2的n次方值,方法和上面同樣。這裏計算出來的cap值就是最終hashentry數組實際的大小了。

初始化就作了這些工做了。

那麼咱們在說說最簡單的get方法。

get方法就須要用到定位咱們的元素了。而定位元素就須要咱們上面初始化時設置好的兩個值:segmentMask和segmentShift

 上面說了,併發度默認值爲16,那麼ssize也爲16,所以segmentMask爲15.因爲ssize二進制往左移了4位,那麼sshift就是4,

segmentShift就是32-4=28.下面咱們就用segmentMask=15,segmentShift爲28來講說怎麼肯定元素位置的。

在這裏咱們要說下hash值,這裏的hash值不是key的hashcode值,而是通過再hash肯定下來的一個hash值,目的是爲了減小hash衝突。

hash值二進制爲32位。

上圖兩個紅框就是分別肯定segment數組中的位置和hashentry數組中的位置。

咱們能夠看到肯定segment數組是採用 (h >>> segmentShift) & segmentMask,其中h爲再hash過的hash值。將32爲的hash值往右移segmentShift位。這裏咱們假設移了28位。

而segmentMask爲15,就是4位都爲一的二進制。將高4位與segmentMask相與會等到一個小於16的值,就是當前元素再的segment位置。

肯定了所屬的segment後。就要確認在的hashentry位置了。經過第二個紅框處,咱們能夠看到肯定hashentry的位置沒有使用上面兩個值了。而是直接使用當前hashentry數組的長度減一

和hash值想與。經過兩種不一樣的算法分別定位segment和hashenrty能夠保證元素在segment數組和hashentry數組裏面都散列開了。

Put方法

public V put(K key, V value) {
        Segment<K,V> s;
        if (value == null)
            throw new NullPointerException();
        int hash = hash(key);
        int j = (hash >>> segmentShift) & segmentMask;
        if ((s = (Segment<K,V>)UNSAFE.getObject          // nonvolatile; recheck
             (segments, (j << SSHIFT) + SBASE)) == null) //  in ensureSegment
            s = ensureSegment(j);
        return s.put(key, hash, value, false);
    }
final V put(K key, int hash, V value, boolean onlyIfAbsent) {
            HashEntry<K,V> node = tryLock() ? null :
                scanAndLockForPut(key, hash, value);
            V oldValue;
            try {
                HashEntry<K,V>[] tab = table;
                int index = (tab.length - 1) & hash;
                HashEntry<K,V> first = entryAt(tab, index);
                for (HashEntry<K,V> e = first;;) {
                    if (e != null) {
                        K k;
                        if ((k = e.key) == key ||
                            (e.hash == hash && key.equals(k))) {
                            oldValue = e.value;
                            if (!onlyIfAbsent) {
                                e.value = value;
                                ++modCount;
                            }
                            break;
                        }
                        e = e.next;
                    }
                    else {
                        if (node != null)
                            node.setNext(first);
                        else
                            node = new HashEntry<K,V>(hash, key, value, first);
                        int c = count + 1;
                        if (c > threshold && tab.length < MAXIMUM_CAPACITY)
                            rehash(node);
                        else
                            setEntryAt(tab, index, node);
                        ++modCount;
                        count = c;
                        oldValue = null;
                        break;
                    }
                }
            } finally {
                unlock();
            }
            return oldValue;
        }

 

上面兩片代碼就是put一個元素的過程。因爲Put方法裏須要對共享變量進行寫入操做,所以爲了安全,須要在操做共享變量時加鎖。put時先定位到segment,而後在segment裏及逆行擦汗如操做。

插入有兩個步驟,第一步判斷是否須要對segment裏的hashenrty數組進行擴容。第二步是定位添加元素的位置,而後將其放在hashenrty數組裏。

咱們先說說擴容。

 

在插入元素的時候會先判斷segment裏面的hashenrty數組是否超過容量threshold。這個容量是咱們剛開始初始化hashenrty數組時採用容量大小和負載因子計算出來的。

若是超過這個閾值(threshold)那麼就會進行擴容。擴容括的時當前hashenrty而不是整個map。

如何擴容

擴容的時候會先建立一個容量是原來兩個容量大小的數組,而後將原數組裏的元素進行再散列後插入到新的數組裏。

Size方法

因爲map裏的元素是遍及全部hashenrty的。所以統計size的時候須要統計每一個hashenrty的大小。因爲是併發環境下,可能出現有線程在插入或者刪除的狀況。所以會出現

錯誤。咱們能想到的就是使用size方法時把全部的segment的put,remove和clean方法都鎖起來。可是這種方法時很低效的。所以concurrenthashmap採用瞭如下辦法:

先嚐試2次經過不加鎖的方式來統計各個segment大小,若是統計的過程當中,容器的count發生了變化,再採用加鎖的方式來統計全部segment的大小。

concurrenthashmap時使用modcount變量來判斷再統計的時候容器是否放生了變化。在put、remove、clean方法裏操做數據前都會將辯能力modCount進行加一,那麼在統計

size千後比較modCount是否發生變化,就能夠知道容器大小是否發生變化了。

相關文章
相關標籤/搜索