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 }