Java7/8中的HashMap和ConcurrentHashMap全解析


1. Java7中的HashMap(key,value均可以為空):

大方向上HashMap是一個數組,每個數組元素是一個單向鏈表。

上圖中每個綠色的實體是嵌套類Entry的實例,Entry包含4個屬性:key,value,hash,和單鏈表的next。

capacity:數組的容量,始終保持在2^n,每次擴容后大小為擴容前的2倍。

loadfactor:擴容因子,始終保持在0.75。

threshold:擴容的閾值,大小為:capacity*loadfactor。

 

1.1put方法的過程:

總結:

當第一次插入時需要初始化數組的大小(threshold);

判斷如果key為空就將這個Entry放入到table[ 0 ]中;

否則計算key的hash值,遍歷單鏈表,若該位置已有元素,就進行覆蓋,並返回舊值;

若不存在重復的值,就將該Entry放入到鏈表中。

 1 public V put(K key, V value) {
 2     // 當插入第一個元素的時候,需要先初始化數組大小
 3     if (table == EMPTY_TABLE) {
 4         inflateTable(threshold);
 5     }
 6     // 如果 key 為 null,感興趣的可以往里看,最終會將這個 entry 放到 table[0] 中
 7     if (key == null)
 8         return putForNullKey(value);
 9     // 1. 求 key 的 hash 值
10     int hash = hash(key);
11     // 2. 找到對應的數組下標
12     int i = indexFor(hash, table.length);
13     // 3. 遍歷一下對應下標處的鏈表,看是否有重復的 key 已經存在,
14     //    如果有,直接覆蓋,put 方法返回舊值就結束了
15     for (Entry<K,V> e = table[i]; e != null; e = e.next) {
16         Object k;
17         if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
18             V oldValue = e.value;
19             e.value = value;
20             e.recordAccess(this);
21             return oldValue;
22         }
23     }
24 
25     modCount++;
26     // 4. 不存在重復的 key,將此 entry 添加到鏈表中,細節后面說
27     addEntry(hash, key, value, i);
28     return null;
29 }

1.2數組(大小)的初始化:

當第一個數組元素放入HashMap時,就進行一次數組的初始化,就是先計算數組的大小,再計算閾值(threshold),並始終將數組內元素的數量保持在2^n個。

 1 private void inflateTable(int toSize) {
 2     // 保證數組大小一定是 2 的 n 次方。
 3     // 比如這樣初始化:new HashMap(20),那么處理成初始數組大小是 32
 4     int capacity = roundUpToPowerOf2(toSize);
 5     // 計算擴容閾值:capacity * loadFactor
 6     threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
 7     // 算是初始化數組吧
 8     table = new Entry[capacity];
 9     initHashSeedAsNeeded(capacity); //ignore
10 }

1.3計算數組的位置:

根據key的Hash值,來對數組長度進行取模。eg:當數組長度為32時,可以取key的hash值的后5位,來進行計算相應數組中位置。

1.4添加結點到鏈表中:

找到數組下標后,進行key判重,若沒有重復,就將該元素放到鏈表的表頭位置。

以下方法首先判斷是否需要擴容,如果擴容后,就將元素放到相應數組位置上鏈表的表頭處。

 1 void addEntry(int hash, K key, V value, int bucketIndex) {
 2     // 如果當前 HashMap 大小已經達到了閾值,並且新值要插入的數組位置已經有元素了,那么要擴容
 3     if ((size >= threshold) && (null != table[bucketIndex])) {
 4         // 擴容,后面會介紹一下
 5         resize(2 * table.length);
 6         // 擴容以后,重新計算 hash 值
 7         hash = (null != key) ? hash(key) : 0;
 8         // 重新計算擴容后的新的下標
 9         bucketIndex = indexFor(hash, table.length);
10     }
11     // 往下看
12     createEntry(hash, key, value, bucketIndex);
13 }
14 // 這個很簡單,其實就是將新值放到鏈表的表頭,然后 size++
15 void createEntry(int hash, K key, V value, int bucketIndex) {
16     Entry<K,V> e = table[bucketIndex];
17     table[bucketIndex] = new Entry<>(hash, key, value, e);
18     size++;
19 }

1.5數組的擴容:

擴容就是將小數組擴大成大數組再將元素轉移到大數組中。雙倍擴容:比如原數組(每個數組中放的其實是一個鏈表)中old [ i]的元素,會放到新數組的new [ i] ,和new [ i+oldlength]的位置上。

 1 void resize(int newCapacity) {
 2     Entry[] oldTable = table;
 3     int oldCapacity = oldTable.length;
 4     if (oldCapacity == MAXIMUM_CAPACITY) {
 5         threshold = Integer.MAX_VALUE;
 6         return;
 7     }
 8     // 新的數組
 9     Entry[] newTable = new Entry[newCapacity];
10     // 將原來數組中的值遷移到新的更大的數組中
11     transfer(newTable, initHashSeedAsNeeded(newCapacity));
12     table = newTable;
13     threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
14 }

1.6 get過程的分析:

 首先根據key計算hash值;

找到相應的數組下標 hash&(length-1);

遍歷數組該位置的鏈表,直到找到相等(==或者equals)的key。

1 public V get(Object key) {
2     // 之前說過,key 為 null 的話,會被放到 table[0],所以只要遍歷下 table[0] 處的鏈表就可以了
3     if (key == null)
4         return getForNullKey();
5     // 
6     Entry<K,V> entry = getEntry(key);
7 
8     return null == entry ? null : entry.getValue();
9 }

getEntry(key):

final Entry<K,V> getEntry(Object key) {
    if (size == 0) {
        //如果數組為空返回null
        return null;
    }
     //根據key計算hash值,通過hash值來判斷數組中的下標位置
    int hash = (key == null) ? 0 : hash(key);
    //確定數組下標后,遍歷該條鏈表直到找到(== / equals)為止
    for (Entry<K,V> e = table[indexFor(hash, table.length)];
         e != null;
         e = e.next) {
        Object k;
        if (e.hash == hash &&
            ((k = e.key) == key || (key != null && key.equals(k))))
            return e;
    }
    return null;
}

2.Java7的concurrentHashMap(value不能為空):

concurrentHashMap支持並發操作,所以比HashMap復雜一點。concurrentHashMap采用分段鎖機制實現線程的同步,concurrentHasMap是由一個個個段組成。

如下圖所示:一個concurrentHashMap是由segment數組構成的,segment繼承了ReentrantLock來實現線程安全,所以只要保證了每個segment的安全性就實現了concurrentHashMap的線程安全。

 

 2.1初始化:

initialCapacity:初始容量,是ConcurrentHashMap的("HashMap的數量"),會平均分給segment;

loadfactor:加載因子,ConcurrentHashMap不可擴容,所以加載因子是給每個segment用的;

concurrenceLevel:可以理解為:並發級別,並發數,segment數,默認值是16,表示一個concurrentHashMap有16個segment,即就是可以允許16個線程來同時寫,concurrenceLevel在初始化時可以指定大小,一旦初始化后不可擴容。(每個segment結構上很像HashMap);

 

初始化完成后(調用new ConcurrentHashMap):

segment數組的默認打小為16;

segment [ i ]的大小為2,加載因子是0.75,所以閾值為1.5,也就是說,當插入第一個元素時,不會擴容,第二個元素時segment[ i ] 會擴容;

並且初始了segment[ 0 ],其他的位置還是null; 

 

 1 public ConcurrentHashMap(int initialCapacity,
 2                          float loadFactor, int concurrencyLevel) {
 3     if (!(loadFactor > 0) || initialCapacity < 0 || concurrencyLevel <= 0)
 4         throw new IllegalArgumentException();
 5     if (concurrencyLevel > MAX_SEGMENTS)
 6         concurrencyLevel = MAX_SEGMENTS;
 7     // Find power-of-two sizes best matching arguments
 8     int sshift = 0;
 9     int ssize = 1;
10     // 計算並行級別 ssize,因為要保持並行級別是 2 的 n 次方
11     while (ssize < concurrencyLevel) {
12         ++sshift;
13         ssize <<= 1;
14     }
15     // 我們這里先不要那么燒腦,用默認值,concurrencyLevel 為 16,sshift 為 4
16     // 那么計算出 segmentShift 為 28,segmentMask 為 15,后面會用到這兩個值
17     this.segmentShift = 32 - sshift;
18     this.segmentMask = ssize - 1;
19 
20     if (initialCapacity > MAXIMUM_CAPACITY)
21         initialCapacity = MAXIMUM_CAPACITY;
22 
23     // initialCapacity 是設置整個 map 初始的大小,
24     // 這里根據 initialCapacity 計算 Segment 數組中每個位置可以分到的大小
25     // 如 initialCapacity 為 64,那么每個 Segment 或稱之為"槽"可以分到 4 個
26     int c = initialCapacity / ssize;
27     if (c * ssize < initialCapacity)
28         ++c;
29     // 默認 MIN_SEGMENT_TABLE_CAPACITY 是 2,這個值也是有講究的,因為這樣的話,對於具體的槽上,
30     // 插入一個元素不至於擴容,插入第二個的時候才會擴容
31     int cap = MIN_SEGMENT_TABLE_CAPACITY; 
32     while (cap < c)
33         cap <<= 1;
34 
35     // 創建 Segment 數組,
36     // 並創建數組的第一個元素 segment[0]
37     Segment<K,V> s0 =
38         new Segment<K,V>(loadFactor, (int)(cap * loadFactor),
39                          (HashEntry<K,V>[])new HashEntry[cap]);
40     Segment<K,V>[] ss = (Segment<K,V>[])new Segment[ssize];
41     // 往數組寫入 segment[0]
42     UNSAFE.putOrderedObject(ss, SBASE, s0); // ordered write of segments[0]
43     this.segments = ss;
44 }

2.2put的過程分析:

主流程:

public V put(K key, V value) {
    Segment<K,V> s;
    if (value == null)
        throw new NullPointerException();
    // 1. 計算 key 的 hash 值
    int hash = hash(key);
    // 2. 根據 hash 值找到 Segment 數組中的位置 j
    //    hash 是 32 位,無符號右移 segmentShift(28) 位,剩下高 4 位,
    //    然后和 segmentMask(15) 做一次與操作,也就是說 j 是 hash 值的高 4 位,也就是槽的數組下標
    int j = (hash >>> segmentShift) & segmentMask;
    // 剛剛說了,初始化的時候初始化了 segment[0],但是其他位置還是 null,
    // ensureSegment(j) 對 segment[j] 進行初始化
    if ((s = (Segment<K,V>)UNSAFE.getObject          // nonvolatile; recheck
         (segments, (j << SSHIFT) + SBASE)) == null) //  in ensureSegment
        s = ensureSegment(j);
    // 3. 插入新值到 槽 s 中
    return s.put(key, hash, value, false);
}

 

根據key來計算hash的值,對應到segment數組的位置,再對segment[ i ]內部進行put操作,segment[ i ]的內部是一個數組+鏈表的形式。 

 1 final V put(K key, int hash, V value, boolean onlyIfAbsent) {
 2     // 在往該 segment 寫入前,需要先獲取該 segment 的獨占鎖
 3     //    先看主流程,后面還會具體介紹這部分內容
 4     HashEntry<K,V> node = tryLock() ? null :
 5         scanAndLockForPut(key, hash, value);
 6     V oldValue;
 7     try {
 8         // 這個是 segment 內部的數組
 9         HashEntry<K,V>[] tab = table;
10         // 再利用 hash 值,求應該放置的數組下標
11         int index = (tab.length - 1) & hash;
12         // first 是數組該位置處的鏈表的表頭
13         HashEntry<K,V> first = entryAt(tab, index);
14 
15         // 下面這串 for 循環雖然很長,不過也很好理解,想想該位置沒有任何元素和已經存在一個鏈表這兩種情況
16         for (HashEntry<K,V> e = first;;) {
17             if (e != null) {
18                 K k;
19                 if ((k = e.key) == key ||
20                     (e.hash == hash && key.equals(k))) {
21                     oldValue = e.value;
22                     if (!onlyIfAbsent) {
23                         // 覆蓋舊值
24                         e.value = value;
25                         ++modCount;
26                     }
27                     break;
28                 }
29                 // 繼續順着鏈表走
30                 e = e.next;
31             }
32             else {
33                 // node 到底是不是 null,這個要看獲取鎖的過程,不過和這里都沒有關系。
34                 // 如果不為 null,那就直接將它設置為鏈表表頭;如果是null,初始化並設置為鏈表表頭。
35                 if (node != null)
36                     node.setNext(first);
37                 else
38                     node = new HashEntry<K,V>(hash, key, value, first);
39 
40                 int c = count + 1;
41                 // 如果超過了該 segment 的閾值,這個 segment 需要擴容
42                 if (c > threshold && tab.length < MAXIMUM_CAPACITY)
43                     rehash(node); // 擴容后面也會具體分析
44                 else
45                     // 沒有達到閾值,將 node 放到數組 tab 的 index 位置,
46                     // 其實就是將新的節點設置成原鏈表的表頭
47                     setEntryAt(tab, index, node);
48                 ++modCount;
49                 count = c;
50                 oldValue = null;
51                 break;
52             }
53         }
54     } finally {
55         // 解鎖
56         unlock();
57     }
58     return oldValue;
59 }

put操作中的關鍵幾步:

初始化段 (ensuresegment):

ConcurrentHashMap初始化時只初始化了第一個segment[ 0 ],其他的segment[ j ],在放第一個元素時進行初始化;當有多個線程進來初始化同一個segment[ i ]時,只會有一個初始化成功(對於並發操作采用CAS算法進行控制該初始化操作)。 

 1 private Segment<K,V> ensureSegment(int k) {
 2     final Segment<K,V>[] ss = this.segments;
 3     long u = (k << SSHIFT) + SBASE; // raw offset
 4     Segment<K,V> seg;
 5     if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u)) == null) {
 6         // 這里看到為什么之前要初始化 segment[0] 了,
 7         // 使用當前 segment[0] 處的數組長度和負載因子來初始化 segment[k]
 8         // 為什么要用“當前”,因為 segment[0] 可能早就擴容過了
 9         Segment<K,V> proto = ss[0];
10         int cap = proto.table.length;
11         float lf = proto.loadFactor;
12         int threshold = (int)(cap * lf);
13 
14         // 初始化 segment[k] 內部的數組
15         HashEntry<K,V>[] tab = (HashEntry<K,V>[])new HashEntry[cap];
16         if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))
17             == null) { // 再次檢查一遍該槽是否被其他線程初始化了。
18 
19             Segment<K,V> s = new Segment<K,V>(lf, threshold, tab);
20             // 使用 while 循環,內部用 CAS,當前線程成功設值或其他線程成功設值后,退出
21             while ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))
22                    == null) {
23                 if (UNSAFE.compareAndSwapObject(ss, u, null, seg = s))
24                     break;
25             }
26         }
27     }
28     return seg;
29 }

獲取寫入鎖(scanAndlockForPut):

在每次寫入到segment前會調用:node = tyrLock()?null :scanAndLockForput(key,value,hash),即首先會進行tryLock()來獲取segment的獨占鎖,若獲取失敗就調用scanAndLockforput(key,value,hash)來獲取鎖。

scanAndLockForPut(key,value,hash)實現控制加鎖:

此方法有兩個出口:

一個是tryLock()成功了退出循環,否則當循環超過一定次數,就會調用lock()--->進入到阻塞等待,直到tryLock()成功!

該方法主要是獲取segment的獨占鎖。

 1 private HashEntry<K,V> scanAndLockForPut(K key, int hash, V value) {
 2     HashEntry<K,V> first = entryForHash(this, hash);
 3     HashEntry<K,V> e = first;
 4     HashEntry<K,V> node = null;
 5     int retries = -1; // negative while locating node
 6 
 7     // 循環獲取鎖
 8     while (!tryLock()) {
 9         HashEntry<K,V> f; // to recheck first below
10         if (retries < 0) {
11             if (e == null) {
12                 if (node == null) // speculatively create node
13                     // 進到這里說明數組該位置的鏈表是空的,沒有任何元素
14                     // 當然,進到這里的另一個原因是 tryLock() 失敗,所以該槽存在並發,不一定是該位置
15                     node = new HashEntry<K,V>(hash, key, value, null);
16                 retries = 0;
17             }
18             else if (key.equals(e.key))
19                 retries = 0;
20             else
21                 // 順着鏈表往下走
22                 e = e.next;
23         }
24         // 重試次數如果超過 MAX_SCAN_RETRIES(單核1多核64),那么不搶了,進入到阻塞隊列等待鎖
25         //    lock() 是阻塞方法,直到獲取鎖后返回
26         else if (++retries > MAX_SCAN_RETRIES) {
27             lock();
28             break;
29         }
30         else if ((retries & 1) == 0 &&
31                  // 這個時候是有大問題了,那就是有新的元素進到了鏈表,成為了新的表頭
32                  //     所以這邊的策略是,相當於重新走一遍這個 scanAndLockForPut 方法
33                  (f = entryForHash(this, hash)) != first) {
34             e = first = f; // re-traverse if entry changed
35             retries = -1;
36         }
37     }
38     return node;
39 }

擴容:rehash

需要注意:segment數組不會擴容,只是對segment數組某一位置上的內部數組(HashEntry <key,value> [ ])進行擴容操作,擴容后的容量為原容量的2倍。在put前會判斷該元素的插入會導致數組元素超過閾值? 如果是,就先擴容(2倍大小)再插入。

以下的方法不需要考慮並發,因為此時還持有segment的獨占鎖。

(也是會將old[ i ]位置上的元素放到new[ i ]和new[ i+old.length])

 1 // 方法參數上的 node 是這次擴容后,需要添加到新的數組中的數據。
 2 private void rehash(HashEntry<K,V> node) {
 3     HashEntry<K,V>[] oldTable = table;
 4     int oldCapacity = oldTable.length;
 5     // 2 倍
 6     int newCapacity = oldCapacity << 1;
 7     threshold = (int)(newCapacity * loadFactor);
 8     // 創建新數組
 9     HashEntry<K,V>[] newTable =
10         (HashEntry<K,V>[]) new HashEntry[newCapacity];
11     // 新的掩碼,如從 16 擴容到 32,那么 sizeMask 為 31,對應二進制 ‘000...00011111’
12     int sizeMask = newCapacity - 1;
13 
14     // 遍歷原數組,老套路,將原數組位置 i 處的鏈表拆分到 新數組位置 i 和 i+oldCap 兩個位置
15     for (int i = 0; i < oldCapacity ; i++) {
16         // e 是鏈表的第一個元素
17         HashEntry<K,V> e = oldTable[i];
18         if (e != null) {
19             HashEntry<K,V> next = e.next;
20             // 計算應該放置在新數組中的位置,
21             // 假設原數組長度為 16,e 在 oldTable[3] 處,那么 idx 只可能是 3 或者是 3 + 16 = 19
22             int idx = e.hash & sizeMask;
23             if (next == null)   // 該位置處只有一個元素,那比較好辦
24                 newTable[idx] = e;
25             else { // Reuse consecutive sequence at same slot
26                 // e 是鏈表表頭
27                 HashEntry<K,V> lastRun = e;
28                 // idx 是當前鏈表的頭結點 e 的新位置
29                 int lastIdx = idx;
30 
31                 // 下面這個 for 循環會找到一個 lastRun 節點,這個節點之后的所有元素是將要放到一起的
32                 for (HashEntry<K,V> last = next;
33                      last != null;
34                      last = last.next) {
35                     int k = last.hash & sizeMask;
36                     if (k != lastIdx) {
37                         lastIdx = k;
38                         lastRun = last;
39                     }
40                 }
41                 // 將 lastRun 及其之后的所有節點組成的這個鏈表放到 lastIdx 這個位置
42                 newTable[lastIdx] = lastRun;
43                 // 下面的操作是處理 lastRun 之前的節點,
44                 //    這些節點可能分配在另一個鏈表中,也可能分配到上面的那個鏈表中
45                 for (HashEntry<K,V> p = e; p != lastRun; p = p.next) {
46                     V v = p.value;
47                     int h = p.hash;
48                     int k = h & sizeMask;
49                     HashEntry<K,V> n = newTable[k];
50                     newTable[k] = new HashEntry<K,V>(h, p.key, v, n);
51                 }
52             }
53         }
54     }
55     // 將新來的 node 放到新數組中剛剛的 兩個鏈表之一 的 頭部
56     int nodeIndex = node.hash & sizeMask; // add the new node
57     node.setNext(newTable[nodeIndex]);
58     newTable[nodeIndex] = node;
59     table = newTable;
60 }

2.3get的過程分析:

根據key計算Hash值;

依據Hash值定位到segment[ i ];

依據Hash值定位到segment[ i ]的內部數組(HashEntry<k,v>[ ])中的某一位置處;

遍歷該數組處的鏈表。

 1 public V get(Object key) {
 2     Segment<K,V> s; // manually integrate access methods to reduce overhead
 3     HashEntry<K,V>[] tab;
 4     // 1. hash 值
 5     int h = hash(key);
 6     long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE;
 7     // 2. 根據 hash 找到對應的 segment
 8     if ((s = (Segment<K,V>)UNSAFE.getObjectVolatile(segments, u)) != null &&
 9         (tab = s.table) != null) {
10         // 3. 找到segment 內部數組相應位置的鏈表,遍歷
11         for (HashEntry<K,V> e = (HashEntry<K,V>) UNSAFE.getObjectVolatile
12                  (tab, ((long)(((tab.length - 1) & h)) << TSHIFT) + TBASE);
13              e != null; e = e.next) {
14             K k;
15             if ((k = e.key) == key || (e.hash == h && key.equals(k)))
16                 return e.value;
17         }
18     }
19     return null;
20 }

2.x並發問題的分析:

get操作是沒有進行加鎖的,添加元素put和刪除元素remove都需要獲取segment的獨占鎖,

這里需要考慮的是在get的過程中遇到put和remove時:

put操作的線程安全性:

初始化段:segment的初始化采用了CAS算法來實現並發控制;

添加結點到鏈表操作是添加到鏈表表頭的;

 3.Java8的HashMap:

Java8對HashMap做了一些修改,最大的不同就是使用了紅黑樹,所以是由(數組,鏈表,紅黑樹)組成的。

在Java7中,當在鏈表中查找目標元素時,時間復雜度是由鏈表長度決定的,為O (n),為了減少這部分的開銷,

zaiJava8中,當鏈表長度達到8時,就將鏈表轉化成紅黑樹的結構,可以降低時間復雜度為O(logN)。

在Java7中使用Entry來存儲HashMap的元素,Java8中采用Node,不過Entry和Node都包含了key,value,hash,next這幾個屬性,Node是適用於鏈表的,TreeNode是紅黑樹。

所以我們可以根據Node和TreeNode來判斷是鏈表還是紅黑樹。

3.1put過程分析:

第一次進行put操作時,會調用resize(),類似於Java7的初始化數組的大小,即從null初始化到16或者指定的大小;

根據key的Hash值來定位到數組的某一位置Node[ i ] ,

若數組該位置沒有元素時就初始化Node,將Node放到鏈表頭部(通過key計算的hash值相同時會將value放到數組的同一位置處,形成鏈表);

若數組的該位置已有元素就比較key的equals(),若equals為true,則進行覆蓋,否則就將該Node放到鏈表的后面,若是樹結構,就調用樹的put;

當插入的元素是鏈表的第八個元素時,就將鏈表轉換成紅黑樹。

 

與Java7的不同:

在擴容時:先插入,再擴容;

當鏈表長度達到8時,就轉為紅黑樹。

 1 public V put(K key, V value) {
 2     return putVal(hash(key), key, value, false, true);
 3 }
 4 
 5 // 第三個參數 onlyIfAbsent 如果是 true,那么只有在不存在該 key 時才會進行 put 操作
 6 // 第四個參數 evict 我們這里不關心
 7 final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
 8                boolean evict) {
 9     Node<K,V>[] tab; Node<K,V> p; int n, i;
10     // 第一次 put 值的時候,會觸發下面的 resize(),類似 java7 的第一次 put 也要初始化數組長度
11     // 第一次 resize 和后續的擴容有些不一樣,因為這次是數組從 null 初始化到默認的 16 或自定義的初始容量
12     if ((tab = table) == null || (n = tab.length) == 0)
13         n = (tab = resize()).length;
14     // 找到具體的數組下標,如果此位置沒有值,那么直接初始化一下 Node 並放置在這個位置就可以了
15     if ((p = tab[i = (n - 1) & hash]) == null)
16         tab[i] = newNode(hash, key, value, null);
17 
18     else {// 數組該位置有數據
19         Node<K,V> e; K k;
20         // 首先,判斷該位置的第一個數據和我們要插入的數據,key 是不是"相等",如果是,取出這個節點
21         if (p.hash == hash &&
22             ((k = p.key) == key || (key != null && key.equals(k))))
23             e = p;
24         // 如果該節點是代表紅黑樹的節點,調用紅黑樹的插值方法,本文不展開說紅黑樹
25         else if (p instanceof TreeNode)
26             e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
27         else {
28             // 到這里,說明數組該位置上是一個鏈表
29             for (int binCount = 0; ; ++binCount) {
30                 // 插入到鏈表的最后面(Java7 是插入到鏈表的最前面)
31                 if ((e = p.next) == null) {
32                     p.next = newNode(hash, key, value, null);
33                     // TREEIFY_THRESHOLD 為 8,所以,如果新插入的值是鏈表中的第 8 個
34                     // 會觸發下面的 treeifyBin,也就是將鏈表轉換為紅黑樹
35                     if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
36                         treeifyBin(tab, hash);
37                     break;
38                 }
39                 // 如果在該鏈表中找到了"相等"的 key(== 或 equals)
40                 if (e.hash == hash &&
41                     ((k = e.key) == key || (key != null && key.equals(k))))
42                     // 此時 break,那么 e 為鏈表中[與要插入的新值的 key "相等"]的 node
43                     break;
44                 p = e;
45             }
46         }
47         // e!=null 說明存在舊值的key與要插入的key"相等"
48         // 對於我們分析的put操作,下面這個 if 其實就是進行 "值覆蓋",然后返回舊值
49         if (e != null) {
50             V oldValue = e.value;
51             if (!onlyIfAbsent || oldValue == null)
52                 e.value = value;
53             afterNodeAccess(e);
54             return oldValue;
55         }
56     }
57     ++modCount;
58     // 如果 HashMap 由於新插入這個值導致 size 已經超過了閾值,需要進行擴容
59     if (++size > threshold)
60         resize();
61     afterNodeInsertion(evict);
62     return null;
63 }

3.2數組擴容:

resize()用來進行數組的初始化,或者擴容,每次擴容后大小為原來的2倍,並進行數據的轉移。

 1 final Node<K,V>[] resize() {
 2     Node<K,V>[] oldTab = table;
 3     int oldCap = (oldTab == null) ? 0 : oldTab.length;
 4     int oldThr = threshold;
 5     int newCap, newThr = 0;
 6     if (oldCap > 0) { // 對應數組擴容
 7         if (oldCap >= MAXIMUM_CAPACITY) {
 8             threshold = Integer.MAX_VALUE;
 9             return oldTab;
10         }
11         // 將數組大小擴大一倍
12         else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
13                  oldCap >= DEFAULT_INITIAL_CAPACITY)
14             // 將閾值擴大一倍
15             newThr = oldThr << 1; // double threshold
16     }
17     else if (oldThr > 0) // 對應使用 new HashMap(int initialCapacity) 初始化后,第一次 put 的時候
18         newCap = oldThr;
19     else {// 對應使用 new HashMap() 初始化后,第一次 put 的時候
20         newCap = DEFAULT_INITIAL_CAPACITY;
21         newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
22     }
23 
24     if (newThr == 0) {
25         float ft = (float)newCap * loadFactor;
26         newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
27                   (int)ft : Integer.MAX_VALUE);
28     }
29     threshold = newThr;
30 
31     // 用新的數組大小初始化新的數組
32     Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
33     table = newTab; // 如果是初始化數組,到這里就結束了,返回 newTab 即可
34 
35     if (oldTab != null) {
36         // 開始遍歷原數組,進行數據遷移。
37         for (int j = 0; j < oldCap; ++j) {
38             Node<K,V> e;
39             if ((e = oldTab[j]) != null) {
40                 oldTab[j] = null;
41                 // 如果該數組位置上只有單個元素,那就簡單了,簡單遷移這個元素就可以了
42                 if (e.next == null)
43                     newTab[e.hash & (newCap - 1)] = e;
44                 // 如果是紅黑樹,具體我們就不展開了
45                 else if (e instanceof TreeNode)
46                     ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
47                 else { 
48                     // 這塊是處理鏈表的情況,
49                     // 需要將此鏈表拆成兩個鏈表,放到新的數組中,並且保留原來的先后順序
50                     // loHead、loTail 對應一條鏈表,hiHead、hiTail 對應另一條鏈表,代碼還是比較簡單的
51                     Node<K,V> loHead = null, loTail = null;
52                     Node<K,V> hiHead = null, hiTail = null;
53                     Node<K,V> next;
54                     do {
55                         next = e.next;
56                         if ((e.hash & oldCap) == 0) {
57                             if (loTail == null)
58                                 loHead = e;
59                             else
60                                 loTail.next = e;
61                             loTail = e;
62                         }
63                         else {
64                             if (hiTail == null)
65                                 hiHead = e;
66                             else
67                                 hiTail.next = e;
68                             hiTail = e;
69                         }
70                     } while ((e = next) != null);
71                     if (loTail != null) {
72                         loTail.next = null;
73                         // 第一條鏈表
74                         newTab[j] = loHead;
75                     }
76                     if (hiTail != null) {
77                         hiTail.next = null;
78                         // 第二條鏈表的新的位置是 j + oldCap,這個很好理解
79                         newTab[j + oldCap] = hiHead;
80                     }
81                 }
82             }
83         }
84     }
85     return newTab;
86 }

 3.3get過程解析:

首先根據key計算hash值,定位到數組的某一位置;

檢查數組該位置處的第一個元素是不是需要get的,不是就進行下一步;

若數組下是紅黑樹結構,就調用樹的方法來取元素;

否則就進行遍歷鏈表,直到找到key的相等(==或者equals());

 1 public V get(Object key) {
 2     Node<K,V> e;
 3     return (e = getNode(hash(key), key)) == null ? null : e.value;
 4 }

5 final Node<K,V> getNode(int hash, Object key) { 6 Node<K,V>[] tab; Node<K,V> first, e; int n; K k; 7 if ((tab = table) != null && (n = tab.length) > 0 && 8 (first = tab[(n - 1) & hash]) != null) { 9 // 判斷第一個節點是不是就是需要的 10 if (first.hash == hash && // always check first node 11 ((k = first.key) == key || (key != null && key.equals(k)))) 12 return first; 13 if ((e = first.next) != null) { 14 // 判斷是否是紅黑樹 15 if (first instanceof TreeNode) 16 return ((TreeNode<K,V>)first).getTreeNode(hash, key); 17 18 // 鏈表遍歷 19 do { 20 if (e.hash == hash && 21 ((k = e.key) == key || (key != null && key.equals(k)))) 22 return e; 23 } while ((e = e.next) != null); 24 } 25 } 26 return null; 27 }

4.Java8的ConcurrentHashMap:

4.1初始化:

無參構造方法:

// 這構造函數里,什么都不干
public ConcurrentHashMap() {
}
public ConcurrentHashMap(int initialCapacity) {
    if (initialCapacity < 0)
        throw new IllegalArgumentException();
    int cap = ((initialCapacity >= (MAXIMUM_CAPACITY >>> 1)) ?
               MAXIMUM_CAPACITY :
               tableSizeFor(initialCapacity + (initialCapacity >>> 1) + 1));
    this.sizeCtl = cap;
}

通過提供的初始容量,計算sizeCtl=(【1.1*initialCapacity+1】,再向上取最近的2^n)。

4.2put的過程分析:

 1 public V put(K key, V value) {
 2     return putVal(key, value, false);
 3 }


4 final V putVal(K key, V value, boolean onlyIfAbsent) { 5 if (key == null || value == null) throw new NullPointerException(); 6 // 得到 hash 值 7 int hash = spread(key.hashCode()); 8 // 用於記錄相應鏈表的長度 9 int binCount = 0; 10 for (Node<K,V>[] tab = table;;) { 11 Node<K,V> f; int n, i, fh; 12 // 如果數組"空",進行數組初始化 13 if (tab == null || (n = tab.length) == 0) 14 // 初始化數組,后面會詳細介紹 15 tab = initTable(); 16 17 // 找該 hash 值對應的數組下標,得到第一個節點 f 18 else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) { 19 // 如果數組該位置為空, 20 // 用一次 CAS 操作將這個新值放入其中即可,這個 put 操作差不多就結束了,可以拉到最后面了 21 // 如果 CAS 失敗,那就是有並發操作,進到下一個循環就好了 22 if (casTabAt(tab, i, null, 23 new Node<K,V>(hash, key, value, null))) 24 break; // no lock when adding to empty bin 25 } 26 // hash 居然可以等於 MOVED,這個需要到后面才能看明白,不過從名字上也能猜到,肯定是因為在擴容 27 else if ((fh = f.hash) == MOVED) 28 // 幫助數據遷移,這個等到看完數據遷移部分的介紹后,再理解這個就很簡單了 29 tab = helpTransfer(tab, f); 30 31 else { // 到這里就是說,f 是該位置的頭結點,而且不為空 32 33 V oldVal = null; 34 // 獲取數組該位置的頭結點的監視器鎖 35 synchronized (f) { 36 if (tabAt(tab, i) == f) { 37 if (fh >= 0) { // 頭結點的 hash 值大於 0,說明是鏈表 38 // 用於累加,記錄鏈表的長度 39 binCount = 1; 40 // 遍歷鏈表 41 for (Node<K,V> e = f;; ++binCount) { 42 K ek; 43 // 如果發現了"相等"的 key,判斷是否要進行值覆蓋,然后也就可以 break 了 44 if (e.hash == hash && 45 ((ek = e.key) == key || 46 (ek != null && key.equals(ek)))) { 47 oldVal = e.val; 48 if (!onlyIfAbsent) 49 e.val = value; 50 break; 51 } 52 // 到了鏈表的最末端,將這個新值放到鏈表的最后面 53 Node<K,V> pred = e; 54 if ((e = e.next) == null) { 55 pred.next = new Node<K,V>(hash, key, 56 value, null); 57 break; 58 } 59 } 60 } 61 else if (f instanceof TreeBin) { // 紅黑樹 62 Node<K,V> p; 63 binCount = 2; 64 // 調用紅黑樹的插值方法插入新節點 65 if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key, 66 value)) != null) { 67 oldVal = p.val; 68 if (!onlyIfAbsent) 69 p.val = value; 70 } 71 } 72 } 73 } 74 75 if (binCount != 0) { 76 // 判斷是否要將鏈表轉換為紅黑樹,臨界值和 HashMap 一樣,也是 8 77 if (binCount >= TREEIFY_THRESHOLD) 78 // 這個方法和 HashMap 中稍微有一點點不同,那就是它不是一定會進行紅黑樹轉換, 79 // 如果當前數組的長度小於 64,那么會選擇進行數組擴容,而不是轉換為紅黑樹 80 // 具體源碼我們就不看了,擴容部分后面說 81 treeifyBin(tab, i); 82 if (oldVal != null) 83 return oldVal; 84 break; 85 } 86 } 87 } 88 // 89 addCount(1L, binCount); 90 return null; 91 }

流程的大概總結:

首先計算key的hash值,定位到數組的某一位置上;

若該數組為空,就進行數組的初始化initTable();

若數組不為空,找到hash值對應的數組下標,並返回第一個結點,若數組的該位置為空,就采用CAS操作將新元素放入到,該位置上put方法就可以宣告結束了。若CAS操作失敗,說明有並發操作,等待下一次的循環就好;

若數該位置處的首元素的hash值為("MOVED"),說明數組正在擴容,幫助數據遷移helpTransfer();

當數組該位置處的頭結點不為空,也不進行擴容時:獲取數組該位置處頭結點的監視器鎖,若頭結點的hash值>0,說明是鏈表結構,遍歷鏈表,看是否需要進行覆蓋,否則就在鏈表的末尾處插入新值;否則為樹結構,調用putTreeval方法插入新值。

若是鏈表結構,再進行判斷是否需要轉換成紅黑樹。

put主要操作的介紹:

初始化數組:(initTable)

初始化一個合適大小的數組,再設置sizeCtl,通過sizeCtl控制CAS操作。

若sizeCtl的值<0,說明有其他線程正在初始化;

否則采用CAS操作進行數組的初始化,將sizeCtl的值設為-1,說明搶到了鎖;

初始化數組,長度為16,或者提供的長度,將這個數組賦值給table,table是Volatile的;

設置sizeCtl的值為sc。

 1 private final Node<K,V>[] initTable() {
 2     Node<K,V>[] tab; int sc;
 3     while ((tab = table) == null || tab.length == 0) {
 4         // 初始化的"功勞"被其他線程"搶去"了
 5         if ((sc = sizeCtl) < 0)
 6             Thread.yield(); // lost initialization race; just spin
 7         // CAS 一下,將 sizeCtl 設置為 -1,代表搶到了鎖
 8         else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
 9             try {
10                 if ((tab = table) == null || tab.length == 0) {
11                     // DEFAULT_CAPACITY 默認初始容量是 16
12                     int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
13                     // 初始化數組,長度為 16 或初始化時提供的長度
14                     Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
15                     // 將這個數組賦值給 table,table 是 volatile 的
16                     table = tab = nt;
17                     // 如果 n 為 16 的話,那么這里 sc = 12
18                     // 其實就是 0.75 * n
19                     sc = n - (n >>> 2);
20                 }
21             } finally {
22                 // 設置 sizeCtl 為 sc,我們就當是 12 吧
23                 sizeCtl = sc;
24             }
25             break;
26         }
27     }
28     return tab;
29 }

鏈表轉紅黑樹:(treeifyBin)

treeifyBin不一定進行紅黑樹的轉換,也可能只是數組的擴容。轉換前先計算鏈表的長度,若長度小於限定值64 ,就只進行擴容操作,否則轉紅黑樹。

 1 private final void treeifyBin(Node<K,V>[] tab, int index) {
 2     Node<K,V> b; int n, sc;
 3     if (tab != null) {
 4         // MIN_TREEIFY_CAPACITY 為 64
 5         // 所以,如果數組長度小於 64 的時候,其實也就是 32 或者 16 或者更小的時候,會進行數組擴容
 6         if ((n = tab.length) < MIN_TREEIFY_CAPACITY)
 7             // 后面我們再詳細分析這個方法
 8             tryPresize(n << 1);
 9         // b 是頭結點
10         else if ((b = tabAt(tab, index)) != null && b.hash >= 0) {
11             // 加鎖
12             synchronized (b) {
13 
14                 if (tabAt(tab, index) == b) {
15                     // 下面就是遍歷鏈表,建立一顆紅黑樹
16                     TreeNode<K,V> hd = null, tl = null;
17                     for (Node<K,V> e = b; e != null; e = e.next) {
18                         TreeNode<K,V> p =
19                             new TreeNode<K,V>(e.hash, e.key, e.val,
20                                               null, null);
21                         if ((p.prev = tl) == null)
22                             hd = p;
23                         else
24                             tl.next = p;
25                         tl = p;
26                     }
27                     // 將紅黑樹設置到數組相應位置中
28                     setTabAt(tab, index, new TreeBin<K,V>(hd));
29                 }
30             }
31         }
32     }
33 }

擴容:(tryPresize)

擴大容量為原先的2倍。

 1 // 首先要說明的是,方法參數 size 傳進來的時候就已經翻了倍了
 2 private final void tryPresize(int size) {
 3     // c:size 的 1.5 倍,再加 1,再往上取最近的 2 的 n 次方。
 4     int c = (size >= (MAXIMUM_CAPACITY >>> 1)) ? MAXIMUM_CAPACITY :
 5         tableSizeFor(size + (size >>> 1) + 1);
 6     int sc;
 7     while ((sc = sizeCtl) >= 0) {
 8         Node<K,V>[] tab = table; int n;
 9 
10         // 這個 if 分支和之前說的初始化數組的代碼基本上是一樣的,在這里,我們可以不用管這塊代碼
11         if (tab == null || (n = tab.length) == 0) {
12             n = (sc > c) ? sc : c;
13             if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
14                 try {
15                     if (table == tab) {
16                         @SuppressWarnings("unchecked")
17                         Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
18                         table = nt;
19                         sc = n - (n >>> 2); // 0.75 * n
20                     }
21                 } finally {
22                     sizeCtl = sc;
23                 }
24             }
25         }
26         else if (c <= sc || n >= MAXIMUM_CAPACITY)
27             break;
28         else if (tab == table) {
29             // 我沒看懂 rs 的真正含義是什么,不過也關系不大
30             int rs = resizeStamp(n);
31 
32             if (sc < 0) {
33                 Node<K,V>[] nt;
34                 if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
35                     sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
36                     transferIndex <= 0)
37                     break;
38                 // 2. 用 CAS 將 sizeCtl 加 1,然后執行 transfer 方法
39                 //    此時 nextTab 不為 null
40                 if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
41                     transfer(tab, nt);
42             }
43             // 1. 將 sizeCtl 設置為 (rs << RESIZE_STAMP_SHIFT) + 2)
44             //     我是沒看懂這個值真正的意義是什么?不過可以計算出來的是,結果是一個比較大的負數
45             //  調用 transfer 方法,此時 nextTab 參數為 null
46             else if (U.compareAndSwapInt(this, SIZECTL, sc,
47                                          (rs << RESIZE_STAMP_SHIFT) + 2))
48                 transfer(tab, null);
49         }
50     }
51 }

數據的遷移:(transfer)

下面這個方法有點長,將原來的 tab 數組的元素遷移到新的 nextTab 數組中。

雖然我們之前說的 tryPresize 方法中多次調用 transfer 不涉及多線程,但是這個 transfer 方法可以在其他地方被調用,典型地,我們之前在說 put 方法的時候就說過了,請往上看 put 方法,是不是有個地方調用了 helpTransfer 方法,helpTransfer 方法會調用 transfer 方法的。

此方法支持多線程執行,外圍調用此方法的時候,會保證第一個發起數據遷移的線程,nextTab 參數為 null,之后再調用此方法的時候,nextTab 不會為 null。

閱讀源碼之前,先要理解並發操作的機制。原數組長度為 n,所以我們有 n 個遷移任務,讓每個線程每次負責一個小任務是最簡單的,每做完一個任務再檢測是否有其他沒做完的任務,幫助遷移就可以了,而 Doug Lea 使用了一個 stride,簡單理解就是步長,每個線程每次負責遷移其中的一部分,如每次遷移 16 個小任務。所以,我們就需要一個全局的調度者來安排哪個線程執行哪幾個任務,這個就是屬性 transferIndex 的作用。

第一個發起數據遷移的線程會將 transferIndex 指向原數組最后的位置,然后從后往前的 stride 個任務屬於第一個線程,然后將 transferIndex 指向新的位置,再往前的 stride 個任務屬於第二個線程,依此類推。當然,這里說的第二個線程不是真的一定指代了第二個線程,也可以是同一個線程,這個讀者應該能理解吧。其實就是將一個大的遷移任務分為了一個個任務包。

  1 private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
  2     int n = tab.length, stride;
  3 
  4     // stride 在單核下直接等於 n,多核模式下為 (n>>>3)/NCPU,最小值是 16
  5     // stride 可以理解為”步長“,有 n 個位置是需要進行遷移的,
  6     //   將這 n 個任務分為多個任務包,每個任務包有 stride 個任務
  7     if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
  8         stride = MIN_TRANSFER_STRIDE; // subdivide range
  9 
 10     // 如果 nextTab 為 null,先進行一次初始化
 11     //    前面我們說了,外圍會保證第一個發起遷移的線程調用此方法時,參數 nextTab 為 null
 12     //       之后參與遷移的線程調用此方法時,nextTab 不會為 null
 13     if (nextTab == null) {
 14         try {
 15             // 容量翻倍
 16             Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1];
 17             nextTab = nt;
 18         } catch (Throwable ex) {      // try to cope with OOME
 19             sizeCtl = Integer.MAX_VALUE;
 20             return;
 21         }
 22         // nextTable 是 ConcurrentHashMap 中的屬性
 23         nextTable = nextTab;
 24         // transferIndex 也是 ConcurrentHashMap 的屬性,用於控制遷移的位置
 25         transferIndex = n;
 26     }
 27 
 28     int nextn = nextTab.length;
 29 
 30     // ForwardingNode 翻譯過來就是正在被遷移的 Node
 31     // 這個構造方法會生成一個Node,key、value 和 next 都為 null,關鍵是 hash 為 MOVED
 32     // 后面我們會看到,原數組中位置 i 處的節點完成遷移工作后,
 33     //    就會將位置 i 處設置為這個 ForwardingNode,用來告訴其他線程該位置已經處理過了
 34     //    所以它其實相當於是一個標志。
 35     ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
 36 
 37 
 38     // advance 指的是做完了一個位置的遷移工作,可以准備做下一個位置的了
 39     boolean advance = true;
 40     boolean finishing = false; // to ensure sweep before committing nextTab
 41 
 42     /*
 43      * 下面這個 for 循環,最難理解的在前面,而要看懂它們,應該先看懂后面的,然后再倒回來看
 44      * 
 45      */
 46 
 47     // i 是位置索引,bound 是邊界,注意是從后往前
 48     for (int i = 0, bound = 0;;) {
 49         Node<K,V> f; int fh;
 50 
 51         // 下面這個 while 真的是不好理解
 52         // advance 為 true 表示可以進行下一個位置的遷移了
 53         //   簡單理解結局:i 指向了 transferIndex,bound 指向了 transferIndex-stride
 54         while (advance) {
 55             int nextIndex, nextBound;
 56             if (--i >= bound || finishing)
 57                 advance = false;
 58 
 59             // 將 transferIndex 值賦給 nextIndex
 60             // 這里 transferIndex 一旦小於等於 0,說明原數組的所有位置都有相應的線程去處理了
 61             else if ((nextIndex = transferIndex) <= 0) {
 62                 i = -1;
 63                 advance = false;
 64             }
 65             else if (U.compareAndSwapInt
 66                      (this, TRANSFERINDEX, nextIndex,
 67                       nextBound = (nextIndex > stride ?
 68                                    nextIndex - stride : 0))) {
 69                 // 看括號中的代碼,nextBound 是這次遷移任務的邊界,注意,是從后往前
 70                 bound = nextBound;
 71                 i = nextIndex - 1;
 72                 advance = false;
 73             }
 74         }
 75         if (i < 0 || i >= n || i + n >= nextn) {
 76             int sc;
 77             if (finishing) {
 78                 // 所有的遷移操作已經完成
 79                 nextTable = null;
 80                 // 將新的 nextTab 賦值給 table 屬性,完成遷移
 81                 table = nextTab;
 82                 // 重新計算 sizeCtl:n 是原數組長度,所以 sizeCtl 得出的值將是新數組長度的 0.75 倍
 83                 sizeCtl = (n << 1) - (n >>> 1);
 84                 return;
 85             }
 86 
 87             // 之前我們說過,sizeCtl 在遷移前會設置為 (rs << RESIZE_STAMP_SHIFT) + 2
 88             // 然后,每有一個線程參與遷移就會將 sizeCtl 加 1,
 89             // 這里使用 CAS 操作對 sizeCtl 進行減 1,代表做完了屬於自己的任務
 90             if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
 91                 // 任務結束,方法退出
 92                 if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
 93                     return;
 94 
 95                 // 到這里,說明 (sc - 2) == resizeStamp(n) << RESIZE_STAMP_SHIFT,
 96                 // 也就是說,所有的遷移任務都做完了,也就會進入到上面的 if(finishing){} 分支了
 97                 finishing = advance = true;
 98                 i = n; // recheck before commit
 99             }
100         }
101         // 如果位置 i 處是空的,沒有任何節點,那么放入剛剛初始化的 ForwardingNode ”空節點“
102         else if ((f = tabAt(tab, i)) == null)
103             advance = casTabAt(tab, i, null, fwd);
104         // 該位置處是一個 ForwardingNode,代表該位置已經遷移過了
105         else if ((fh = f.hash) == MOVED)
106             advance = true; // already processed
107         else {
108             // 對數組該位置處的結點加鎖,開始處理數組該位置處的遷移工作
109             synchronized (f) {
110                 if (tabAt(tab, i) == f) {
111                     Node<K,V> ln, hn;
112                     // 頭結點的 hash 大於 0,說明是鏈表的 Node 節點
113                     if (fh >= 0) {
114                         // 下面這一塊和 Java7 中的 ConcurrentHashMap 遷移是差不多的,
115                         // 需要將鏈表一分為二,
116                         //   找到原鏈表中的 lastRun,然后 lastRun 及其之后的節點是一起進行遷移的
117                         //   lastRun 之前的節點需要進行克隆,然后分到兩個鏈表中
118                         int runBit = fh & n;
119                         Node<K,V> lastRun = f;
120                         for (Node<K,V> p = f.next; p != null; p = p.next) {
121                             int b = p.hash & n;
122                             if (b != runBit) {
123                                 runBit = b;
124                                 lastRun = p;
125                             }
126                         }
127                         if (runBit == 0) {
128                             ln = lastRun;
129                             hn = null;
130                         }
131                         else {
132                             hn = lastRun;
133                             ln = null;
134                         }
135                         for (Node<K,V> p = f; p != lastRun; p = p.next) {
136                             int ph = p.hash; K pk = p.key; V pv = p.val;
137                             if ((ph & n) == 0)
138                                 ln = new Node<K,V>(ph, pk, pv, ln);
139                             else
140                                 hn = new Node<K,V>(ph, pk, pv, hn);
141                         }
142                         // 其中的一個鏈表放在新數組的位置 i
143                         setTabAt(nextTab, i, ln);
144                         // 另一個鏈表放在新數組的位置 i+n
145                         setTabAt(nextTab, i + n, hn);
146                         // 將原數組該位置處設置為 fwd,代表該位置已經處理完畢,
147                         //    其他線程一旦看到該位置的 hash 值為 MOVED,就不會進行遷移了
148                         setTabAt(tab, i, fwd);
149                         // advance 設置為 true,代表該位置已經遷移完畢
150                         advance = true;
151                     }
152                     else if (f instanceof TreeBin) {
153                         // 紅黑樹的遷移
154                         TreeBin<K,V> t = (TreeBin<K,V>)f;
155                         TreeNode<K,V> lo = null, loTail = null;
156                         TreeNode<K,V> hi = null, hiTail = null;
157                         int lc = 0, hc = 0;
158                         for (Node<K,V> e = t.first; e != null; e = e.next) {
159                             int h = e.hash;
160                             TreeNode<K,V> p = new TreeNode<K,V>
161                                 (h, e.key, e.val, null, null);
162                             if ((h & n) == 0) {
163                                 if ((p.prev = loTail) == null)
164                                     lo = p;
165                                 else
166                                     loTail.next = p;
167                                 loTail = p;
168                                 ++lc;
169                             }
170                             else {
171                                 if ((p.prev = hiTail) == null)
172                                     hi = p;
173                                 else
174                                     hiTail.next = p;
175                                 hiTail = p;
176                                 ++hc;
177                             }
178                         }
179                         // 如果一分為二后,節點數少於 8,那么將紅黑樹轉換回鏈表
180                         ln = (lc <= UNTREEIFY_THRESHOLD) ? untreeify(lo) :
181                             (hc != 0) ? new TreeBin<K,V>(lo) : t;
182                         hn = (hc <= UNTREEIFY_THRESHOLD) ? untreeify(hi) :
183                             (lc != 0) ? new TreeBin<K,V>(hi) : t;
184 
185                         // 將 ln 放置在新數組的位置 i
186                         setTabAt(nextTab, i, ln);
187                         // 將 hn 放置在新數組的位置 i+n
188                         setTabAt(nextTab, i + n, hn);
189                         // 將原數組該位置處設置為 fwd,代表該位置已經處理完畢,
190                         //    其他線程一旦看到該位置的 hash 值為 MOVED,就不會進行遷移了
191                         setTabAt(tab, i, fwd);
192                         // advance 設置為 true,代表該位置已經遷移完畢
193                         advance = true;
194                     }
195                 }
196             }
197         }
198     }
199 }

4.3get過程的分析:

計算key的hash值,根據hash值定位到數組的某一位置;

根據該位置處結點的性質進行查找:

若該位置為null,直接返回null;

若該位置處的結點是要get的,就返回該節點的值即可;

若該位置結點的hash值小於0,說明正在進行擴容,或者是樹結構;

否則,就是鏈表結構,直接進行對比key值即可。

 1 public V get(Object key) {
 2     Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
 3     int h = spread(key.hashCode());
 4     if ((tab = table) != null && (n = tab.length) > 0 &&
 5         (e = tabAt(tab, (n - 1) & h)) != null) {
 6         // 判斷頭結點是否就是我們需要的節點
 7         if ((eh = e.hash) == h) {
 8             if ((ek = e.key) == key || (ek != null && key.equals(ek)))
 9                 return e.val;
10         }
11         // 如果頭結點的 hash 小於 0,說明 正在擴容,或者該位置是紅黑樹
12         else if (eh < 0)
13             // 參考 ForwardingNode.find(int h, Object k) 和 TreeBin.find(int h, Object k)
14             return (p = e.find(h, key)) != null ? p.val : null;
15 
16         // 遍歷鏈表
17         while ((e = e.next) != null) {
18             if (e.hash == h &&
19                 ((ek = e.key) == key || (ek != null && key.equals(ek))))
20                 return e.val;
21         }
22     }
23     return null;
24 }

 


免責聲明!

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



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