分析1.七、1.8的HashMap、ConcurrentHashMap的區別html
越到後面越煩躁,寫不下去了,到時回來填坑把java
1. 補充位運算
位運算是對2進制而言的node
符號 | 描述 | 運算規則 |
---|---|---|
& | 與 | 兩個都爲1,結果才爲1 |
| | 或 | 兩個都爲0,結果纔是0 |
^ | 異或 | 同0異1 |
~ | 取反 | 0變1,1變0 |
<< | 左移 | 各二進位所有左移若干位,高位丟棄,低位補0。表示2次冪 |
>> | 右移 | 各二進位所有右移若干位,對無符號數,高位補0。表示除2 |
有符號數,各編譯器處理方法不同,有的補符號位(算術右移),有的補0(邏輯右移) | ||
>>> | 邏輯右移 | 無論符號位,直接右移,補零 |
1.1 位運算實現取模
// 其實很簡單,主要看length // &運算,就算h所有爲1,&以後都是看length有1的部分 // 那麼最大隻能是length,因此範圍限定在了length裏,比 % 運算快多了 // -1爲了符合數組0開始 // 這也是擴容爲2次冪的緣由,配套取模運算 h & (length-1)
1.2 二次冪
二次冪的二進制只有一個1,其餘位爲0數組
若是減1,那麼二進制中的1變成0,後面的0所有變成1,符合上面的length,配合實現取模運算安全
1---0001 2---0010 4---0100 8---1000
2. JDK 1.7
使用頭插法,鏈表放頭部是最快的,尾部須要遍歷的多線程
真要put的才初始化,體現懶加載併發
2.1 構造函數
public HashMap(int initialCapacity, float loadFactor) { // 參數的各類判斷 if (initialCapacity < 0) throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity); if (initialCapacity > MAXIMUM_CAPACITY) initialCapacity = MAXIMUM_CAPACITY; if (loadFactor <= 0 || Float.isNaN(loadFactor)) throw new IllegalArgumentException("Illegal load factor: " + loadFactor); // 設置加載因子,體現懶加載 this.loadFactor = loadFactor; // 這裏注意講初始化容量大小,賦值給閥值,後面擴容表用到 // 其實也就是擴容大小,不過爲何用閥值賦值?? threshold = initialCapacity; // 真正的初始化 init(); }
2.2 二次冪
// 總之返回一個大於但最接近number的二次冪 private static int roundUpToPowerOf2(int number) { // assert number >= 0 : "number must be non-negative"; return number >= MAXIMUM_CAPACITY // -1保證自己不是2次冪,左移保證大於大於當前2次冪,而小於下一個2次冪 ? MAXIMUM_CAPACITY : (number > 1) ? Integer.highestOneBit((number - 1) << 1) : 1; } // 返回小於等於的2次冪 public static int highestOneBit(int i) { // 屢次右移再異或保證最高位的1後面全是1 i |= (i >> 1); i |= (i >> 2); i |= (i >> 4); i |= (i >> 8); i |= (i >> 16); // 這裏保證除了最高位1覺得,其他所有變成0了 return i - (i >>> 1); }
2.3 擴容表
private void inflateTable(int toSize) { // Find a power of 2 >= toSize int capacity = roundUpToPowerOf2(toSize); // 和最大容量比咯,選擇小的。。。 threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1); // 建立哈希表的桶子 table = new Entry[capacity]; // 是否再哈希 initHashSeedAsNeeded(capacity); }
2.4 再哈希
// 正常來講都不用 final boolean initHashSeedAsNeeded(int capacity) { // 哈希種子,默認0,返回false boolean currentAltHashing = hashSeed != 0; boolean useAltHashing = sun.misc.VM.isBooted() && // 最關鍵在這裏,這裏要容量容許的最大值時才爲true // Holder裏面從JVM環境配置拿看有沒有設置自定義的最大容量 (capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD); // false false boolean switching = currentAltHashing ^ useAltHashing; // false,意思不用再hash if (switching) { hashSeed = useAltHashing ? sun.misc.Hashing.randomHashSeed(this) : 0; } return switching; }
2.5 哈希值
// 注意:根據Key來獲取hash值的 final int hash(Object k) { int h = hashSeed; if (0 != h && k instanceof String) { return sun.misc.Hashing.stringHash32((String) k); } // 根據Key的哈希值再和哈希散列 h ^= k.hashCode(); h ^= (h >>> 20) ^ (h >>> 12); return h ^ (h >>> 7) ^ (h >>> 4); }
2.6 獲取下標
static int indexFor(int h, int length) { // 取模,必須確保2次冪 return h & (length-1); }
2.7 get方法
public V get(Object key) { // 固然首先判斷是否是空,空就調用專門對NULL的方法,1.7有專門分開 if (key == null) return getForNullKey(); // 不然調用正常流程 Entry<K,V> entry = getEntry(key); // Entry是從Tree裏面繼承的,K/V結構 return null == entry ? null : entry.getValue(); } // 看看NULL的實現 private V getForNullKey() { // 若是空表,返回null把 if (size == 0) { return null; } // null 是固定放在0號下標的 for (Entry<K,V> e = table[0]; e != null; e = e.next) { if (e.key == null) return e.value; } return null; } // 正常流程 final Entry<K,V> getEntry(Object key) { // 固然也有非空判斷 if (size == 0) { return null; } // 這裏的null多是直接調用,不用經過get的把 // 好比containsKey:return getEntry(key) != null; int hash = (key == null) ? 0 : hash(key); // 流程纔是重點 // 1.根據Key的哈希值獲取桶子下標,而後遍歷該桶子上的元素進行判斷 // 2.判斷流程: // 2.1 判斷元素的哈希值 // 2.2 再判斷Key是否地址相同(好比String有常量池),或者值相同 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.8 put方法
// 也是注意流程 // 1.根據Key獲取桶下標進行遍歷,有相同的就替換 // 2.沒有相同則插入 // 2.2 public V put(K key, V value) { // 體現懶加載,添加元素纔去看看初始化沒 if (table == EMPTY_TABLE) { // 前面函數有說明,擴容表 // 看這裏傳進入的是閥值,也就是第一次賦值時傳進去的容量大小 inflateTable(threshold); } // 看來爲空都有特殊對待 if (key == null) return putForNullKey(value); // 獲取hash值,以及映射對應的獲取桶子下標 int hash = hash(key); int i = indexFor(hash, table.length); // 鏈表遍歷咯,看是否有相同的,相同則替換 for (Entry<K,V> e = table[i]; e != null; e = e.next) { Object k; // 步驟都差很少 // 第一步比hash,第二步比key地址,第三步比key值 if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { // 若是已經存在值了,那麼替換,返回舊值 V oldValue = e.value; e.value = value; // 這裏是空,主要給LinkedHashMap記錄訪問順序的 e.recordAccess(this); // 返回舊值 return oldValue; } } // 快速失敗機制 modCount++; // 沒有重複的值,那麼就插入 addEntry(hash, key, value, i); return null; }
2.8.1 插入空值流程
// 因此這裏存在的就是爲了減小運算?? // 直接從0下標遍歷,麻煩 private V putForNullKey(V value) { // 區別在於直接從0下標遍歷,由於NULL只存0下標 // 也是先遍歷,看有沒有存在替換,沒有就插入 for (Entry<K,V> e = table[0]; e != null; e = e.next) { if (e.key == null) { V oldValue = e.value; e.value = value; e.recordAccess(this); return oldValue; } } // 少不了的套路,快速失敗 modCount++; // 這裏纔是真正的插入方法 addEntry(0, null, value, 0); return null; }
2.8.2 插入非NULL值流程
// 這裏仍是添加Entry的判斷(判斷條件也挺重要的) void addEntry(int hash, K key, V value, int bucketIndex) { // 判斷閥值,且該桶子上不爲空 // 爲空就不擴容,爲了節省了一次擴容消耗??? // 這裏是1.7的特色,目標下標爲空能夠直接插入,不進行擴容 if ((size >= threshold) && (null != table[bucketIndex])) { // 擴容後面講,知道這裏2倍就是了 // 擴容完,固然還要記得插入元素 resize(2 * table.length); hash = (null != key) ? hash(key) : 0; bucketIndex = indexFor(hash, table.length); } // 正真的頭插入過程 createEntry(hash, key, value, bucketIndex); }
2.8.3 真正的插入元素
// 1.7 使用頭插法,併發會出現死循環,1.8 使用尾插法 // 下面是頭插法的流程,慢慢看,可能有點難懂 void createEntry(int hash, K key, V value, int bucketIndex) { // 先獲取桶子指定下標桶的元素 Entry<K,V> e = table[bucketIndex]; // 而後將桶子上的元素換成將要插入的元素 // 被頂掉的元素放入,插入元素的next屬性上就OK了 table[bucketIndex] = new Entry<>(hash, key, value, e); // 最後別忘實際存放的元素數量+1 size++; }
2.9 再散列(擴容)
// 擴容理由: // 桶子不夠,哈希衝突過多 // 鏈表過長,影響get遍歷效率 // 注意流程: // 1.建新表,兩倍大小 // 2.數據轉移 // 2.1 遍歷桶子的同時,遍歷桶子上的鏈表 // 2.2 而後逐個拆分鏈表元素,再移動新舊錶上。(1.8是拼接新舊拼接好後才移動的) // 3.新表替舊錶 void resize(int newCapacity) { // 舊桶 Entry[] oldTable = table; // 舊容量 int oldCapacity = oldTable.length; // 若是已經最大了,那麼設置閥值,而後什麼都不作直接返回 if (oldCapacity == MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; return; } // 建立一個新桶,2倍大小的 Entry[] newTable = new Entry[newCapacity]; // 數據轉移,第二個參數新容量決定是否再哈希 transfer(newTable, initHashSeedAsNeeded(newCapacity)); // 新表替舊錶 table = newTable; // 設置新閥值,其實能夠直接*2的 threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1); }
2.9.1 數據轉移
void transfer(Entry[] newTable, boolean rehash) { // 取出新桶容量 int newCapacity = newTable.length; // 遍歷舊桶 for (Entry<K,V> e : table) { // 遍歷每一個桶子上的鏈表 while(null != e) { // 這裏獲取了當前遍歷的e(Entry),以及e後一個Entry,就是遍歷鏈表 Entry<K,V> next = e.next; // 判斷需不須要再哈希 if (rehash) { e.hash = null == e.key ? 0 : hash(e.key); } // 在同一個桶子上,證實hash取模容量以後相同,只是下標相同,不必定hash相同 // 若是容量變化了,那麼取模以後這些同鏈表的元素就會拆分 // 這裏擴容完: // 問題一:鏈表由於頭插法,倒序了 // 問題二:多線程添加元素擴容,死鎖。線程1元素丟失,線程2死循環 // 下面是頭插法 // 獲取新下標,由於桶長變了 int i = indexFor(e.hash, newCapacity); // 這步和下一步共同組成移到桶子上方,而後再下移 e.next = newTable[i]; // 而後再往下移動 newTable[i] = e; // 這個操做和上面合起來至關於:e = e.next, e = next; } } }
2.9.2 多線程擴容
// 舊哈希值兩個線程共有 // 對於新表,兩個線程各自會建立一個新表 // 轉移到新表的時候,頭插法混亂,線程1還沒移動完成,線程2就開始移動,兩者的鏈表會造成環,從而死循環
2.10 ConcurrentHashMap
併發的HashMap,使用了ReentrantLock,下面看原理app
3.10.1 原理
// 有內部類Segment,繼承了ReentranLock,且是個小的HashMap,那麼每一個分段擴容不一樣大小也就不一樣了 // 內有兩數組,Segment[]、Entry[] // Segment[]:實現了分段鎖機制,往數組插入防止併發要用到CAS // Entry[]:是咱們真正存放元素的 // 意思就是Segment數組裏面有個Entry數組,要往Entry放元素,得先獲取Segment的鎖
2.10.2 屬性
// 併發級別,默認16 // 就是總共有多少個鎖,分多少段(Segment),繼承ReentrantLock static final int DEFAULT_CONCURRENCY_LEVEL = 16; // 最多分段 static final int MAX_SEGMENTS = 1 << 16; // slightly conservative
2.10.3 構造方法
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; // 找大於等於併發級別的2次冪最小值 while (ssize < concurrencyLevel) { // 記錄併發級別的二進制位數 ++sshift; ssize <<= 1; } // 這裏保留併發級別相關的位數,後面左移用到了 this.segmentShift = 32 - sshift; // 這個用來取模找Segment數組下標 this.segmentMask = ssize - 1; if (initialCapacity > MAXIMUM_CAPACITY) initialCapacity = MAXIMUM_CAPACITY; // 這裏看每一個Segment存多少個 int c = initialCapacity / ssize; // 除法的向上取整 if (c * ssize < initialCapacity) ++c; // cap最小默認爲2,這裏是Segment裏的HashMap用的容量大小,固然須要2次冪了 int cap = MIN_SEGMENT_TABLE_CAPACITY; while (cap < c) cap <<= 1; // create segments and segments[0] // 建立一個S0的分段,不用每次都計算分段內HashMap的大小了 Segment<K,V> s0 = // 加載因子,閥值,table建好放進去給你了 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; }
2.10.4 put方法
public V put(K key, V value) { Segment<K,V> s; if (value == null) throw new NullPointerException(); int hash = hash(key); // 根據hash值,獲取Segment數組的下標 int j = (hash >>> segmentShift) & segmentMask; // 獲取對應下標的數組元素,判斷是否爲空 if ((s = (Segment<K,V>)UNSAFE.getObject(segments, (j << SSHIFT) + SBASE)) == null) // 爲空建立小HashMap咯 s = ensureSegment(j); return s.put(key, hash, value, false); }
2.10.5 生成Segment對象
private Segment<K,V> ensureSegment(int k) { // 又來一次判斷?? final Segment<K,V>[] ss = this.segments; long u = (k << SSHIFT) + SBASE; // raw offset Segment<K,V> seg; // 看看有沒有其餘線程已經生成新的了,UNSAF安全類 if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u)) == null) { // 從原型獲取 Segment<K,V> proto = ss[0]; // use segment 0 as prototype int cap = proto.table.length; float lf = proto.loadFactor; int threshold = (int)(cap * lf); HashEntry<K,V>[] tab = (HashEntry<K,V>[])new HashEntry[cap]; // 又判斷是否是空,再次檢查 if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u)) == null) { // new了一個Segment對象 Segment<K,V> s = new Segment<K,V>(lf, threshold, tab); // 判空才自旋CAS添加第一層數組元素 while ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u)) == null) { // 根據CAS操做交換的 if (UNSAFE.compareAndSwapObject(ss, u, null, seg = s)) break; } } } // 這裏不論是否本身建立的,都會返回一個Segment對象,前面有屢次獲取最新。。。 return seg; }
2.10.6 Segment的put方法
// Segment插入固然使用繼承的ReentranLock final V put(K key, int hash, V value, boolean onlyIfAbsent) { // Seg對象的put方法會有鎖,tryLock不阻塞的,和Lock比 // tryLock失敗,能夠用後面的獲取鎖方法 HashEntry<K,V> node = tryLock() ? null : scanAndLockForPut(key, hash, value); V oldValue; try { // 獲取該Seg對應的table HashEntry<K,V>[] tab = table; int index = (tab.length - 1) & hash; // 獲取對應的table下標,返回第一個Entry,後面劇情應該是遍歷是否重複,而後頭插法 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); // 小HashMap的實際大小 int c = count + 1; // 內部擴容機制 if (c > threshold && tab.length < MAXIMUM_CAPACITY) rehash(node); else // 這個set內部用Unsafe方法,不然改的是線程的,不是內存的,可能仍是有衝突 setEntryAt(tab, index, node); ++modCount; count = c; oldValue = null; break; } } } finally { unlock(); } return oldValue; }
2.10.7 獲取鎖函數
private HashEntry<K,V> scanAndLockForPut(K key, int hash, V value) { // 獲取table上對應的Entry HashEntry<K,V> first = entryForHash(this, hash); HashEntry<K,V> e = first; HashEntry<K,V> node = null; // 重試次數 int retries = -1; while (!tryLock()) { HashEntry<K,V> f; // to recheck first below // 先遍歷判斷要不要new一個Entry if (retries < 0) { if (e == null) { if (node == null) // speculatively create node node = new HashEntry<K,V>(hash, key, value, null); retries = 0; } else if (key.equals(e.key)) retries = 0; else e = e.next; } // 超太重試次數,直接lock()便可能被阻塞 else if (++retries > MAX_SCAN_RETRIES) { lock(); break; } // 判斷獲取的頭部,和最新的頭部是否相同。這裏就是判斷別人獲取鎖時,有否改變結構。 else if ((retries & 1) == 0 && (f = entryForHash(this, hash)) != first) { e = first = f; // re-traverse if entry changed retries = -1; } } return node; }
2.10.8 擴容
private void rehash(HashEntry<K,V> node) { HashEntry<K,V>[] oldTable = table; int oldCapacity = oldTable.length; int newCapacity = oldCapacity << 1; threshold = (int)(newCapacity * loadFactor); HashEntry<K,V>[] newTable = (HashEntry<K,V>[]) new HashEntry[newCapacity]; int sizeMask = newCapacity - 1; // 遍歷轉移咯 for (int i = 0; i < oldCapacity ; i++) { HashEntry<K,V> e = oldTable[i]; if (e != null) { HashEntry<K,V> next = e.next; // 直接獲取了新下標,不用判斷是否重哈希 int idx = e.hash & sizeMask; // 只有一個元素直接放進桶子 if (next == null) newTable[idx] = e; // else { // Reuse consecutive sequence at same slot HashEntry<K,V> lastRun = e; int lastIdx = idx; // 這個循環爲了:記錄最後下標相同的元素,轉移就一塊兒轉移過去了 // 只是最後的,前面的和後面的就被覆蓋了,即沒有記錄到 for (HashEntry<K,V> last = next;last != null;last = last.next) { int k = last.hash & sizeMask; if (k != lastIdx) { lastIdx = k; lastRun = last; } } newTable[lastIdx] = lastRun; // Clone remaining nodes for (HashEntry<K,V> p = e; p != lastRun; p = p.next) { V v = p.value; int h = p.hash; int k = h & sizeMask; HashEntry<K,V> n = newTable[k]; newTable[k] = new HashEntry<K,V>(h, p.key, v, n); } } } } // 秉持1.7先擴容再插入元素 int nodeIndex = node.hash & sizeMask; // add the new node node.setNext(newTable[nodeIndex]); newTable[nodeIndex] = node; table = newTable; }
3. JDK 1.8
- 判斷是否空
- get
- 判斷第一個元素
- 而後下一個再判斷是否紅黑樹
- 不是紅黑樹就用鏈表的方法
- put
- 判斷表是否爲空,爲空初始化
- 判斷對應的桶子上是否爲空,爲空直接插入
- 判斷是否匹配第一個元素,是就直接替換
- 不然判斷是否紅黑樹,不然進入鏈表處理階段(循環遍歷看是否有重複元素)
- 有重複元素退出循環,進入值替換
- put以後,再判斷是否須要resize ()
- 判斷節點是否相等。先比Hash值,再比地址(適用null),最後再比key值
3.1 變量
// 初始化容量大小16 static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 最大容量2^30 static final int MAXIMUM_CAPACITY = 1 << 30; // 默認加載因子0.75 static final float DEFAULT_LOAD_FACTOR = 0.75f; // 轉化紅黑樹界限8 static final int TREEIFY_THRESHOLD = 8; // 轉化成鏈表界限6,兩者不一樣是爲了避免要在界限內一直轉換 static final int UNTREEIFY_THRESHOLD = 6; // 當鏈表須要轉換成紅黑樹時,先先判斷數組長度是否<64,是的話不轉換紅黑樹,先擴容 static final int MIN_TREEIFY_CAPACITY = 64;
3.2 節點
// Node實現了Entry,以前用的是Entry static class Node<K,V> implements Map.Entry<K,V> { final int hash; final K key; V value; Node<K,V> next; Node(int hash, K key, V value, Node<K,V> next) { this.hash = hash; this.key = key; this.value = value; this.next = next; } public final K getKey() { return key; } public final V getValue() { return value; } public final String toString() { return key + "=" + value; } public final int hashCode() { return Objects.hashCode(key) ^ Objects.hashCode(value); } public final V setValue(V newValue) { V oldValue = value; value = newValue; return oldValue; } public final boolean equals(Object o) { if (o == this) return true; if (o instanceof Map.Entry) { Map.Entry<?,?> e = (Map.Entry<?,?>)o; if (Objects.equals(key, e.getKey()) && Objects.equals(value, e.getValue())) return true; } return false; } }
3.3 哈希值
// 根據key生成, >>>無符號舍棄右邊16位,即取高16位 // 由於通常桶長度都在2字節下, static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); } // n = tab.length // 通常桶長度都小於2^16即小於65536,即低16位纔有效 // 這樣與哈希異或的話,只能用到了低16位的 // 那麼上面hash自身與自身的高16位異或,就利用到了高16位,更隨機 tab[(n - 1) & hash] // 返回桶下標
3.4 二次冪
// 1.8大於輸入參數且最近的2的整數次冪的數,而1.7是小於等於的 // 或,右移運算 // 這樣作爲了讓二進制中,最高位的1後面全置爲1,後面加1轉換爲從1開始計數,不是從0開始了 static final int tableSizeFor(int cap) { int n = cap - 1; // 防止已是2次冪的狀況 n |= n >>> 1; n |= n >>> 2; n |= n >>> 4; n |= n >>> 8; n |= n >>> 16; return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1; }
3.5 字段
transient Node<K,V>[] table; transient Set<Map.Entry<K,V>> entrySet; // 實際存儲元素多少 transient int size; // 記錄 hashMap 發生結構性變化的次數 // 調用迭代器的時候,將modCount賦值給迭代器內部 // 若是修改告終構,modCount就會+1 // 那麼迭代器迭代一次,就會判斷內部的expectedModCount 和 HashMap的是否相同,不一樣則拋出異常 // 調用Iterator的remove方法便可,內部就是從新賦值迭代器內部的modCount而已 transient int modCount; // 值等於table.length * loadFactor, size 超過這個值時進行 resize()擴容 int threshold; // 加載因子 final float loadFactor;
3.6 構造函數
public HashMap(int initialCapacity, float loadFactor) { // 小於0固然拋出異常 if (initialCapacity < 0) throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity); // 不能最大2^30 if (initialCapacity > MAXIMUM_CAPACITY) initialCapacity = MAXIMUM_CAPACITY; // 加載因子異常 if (loadFactor <= 0 || Float.isNaN(loadFactor)) throw new IllegalArgumentException("Illegal load factor: " + loadFactor); // 構造函數只是賦值,沒有初始化表,懶加載 this.loadFactor = loadFactor; // 前面的2次冪用處,這裏就設置了閥值,用於後面的擴容,其實就是賦值容量給閥值 this.threshold = tableSizeFor(initialCapacity); // 沒有了那個LinkedHashMap的訪問順序初始化函數 } // 給了初始化大小,加載因子 public HashMap(int initialCapacity) { this(initialCapacity, DEFAULT_LOAD_FACTOR); } // 就給了加載因子 public HashMap() { this.loadFactor = DEFAULT_LOAD_FACTOR; }
3.7 簡單函數
public int size() { return size; } public boolean isEmpty() { return size == 0; } public boolean containsKey(Object key) { return getNode(hash(key), key) != null; }
3.8 get方法
public V get(Object key) { Node<K,V> e; // 這裏返回節點的.value return (e = getNode(hash(key), key)) == null ? null : e.value; } // 這裏返回節點 final Node<K,V> getNode(int hash, Object key) { Node<K,V>[] tab; Node<K,V> first, e; int n; K k; // 桶不爲空,桶大於0,獲取桶下標 if((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null){ // 老是先對比桶上第一個元素,地址相同,或者內容相同(爲了省去equal而先對比地址?) if (first.hash == hash && ((k = first.key) == key || (key != null && key.equals(k)))) return first; // 循環遍歷鏈表,第一個元素都是被放棄遍歷的嗎。。。。 if ((e = first.next) != null) { // 查看是否紅黑樹(難點,重點,考點) if (first instanceof TreeNode) // 強轉,而後.getTreeNode方法 return ((TreeNode<K,V>)first).getTreeNode(hash, key); // 這裏非紅黑樹就是鏈表了,遍歷咯,找到就返回,沒找到null do { if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) return e; } while ((e = e.next) != null); } } return null; }
3.9 put方法
public V put(K key, V value) { // 第三個參數:onlyIfAbsent,第四個:訪問順序用的 return putVal(hash(key), key, value, false, true); }
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { // tab存放表,p存放節點,n表示桶長,i表示哈希值 Node<K,V>[] tab; Node<K,V> p; int n, i; // 空表時resize if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; // 對應桶上爲空,直接放桶上 // tab[i = (n - 1) & hash],至關於 hash % n // 但通常是取最大的素數來取模的 if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); // 這裏就是發生哈希衝突 else { Node<K,V> e; K k; // 首先判斷是否第一個元素,是的話賦值給e,直接進入替換值的階段,跳過下面兩個步驟 if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p; // 進入紅黑樹處理階段 else if (p instanceof TreeNode) e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); // 鏈表處理階段 else { // 鏈表遍歷 for (int binCount = 0; ; ++binCount) { // 遍歷到一個空的,那就尾部插入,這裏和1.7的頭插法不一樣 // 一旦插入成功,那麼退出這個循環 if ((e = p.next) == null) { p.next = newNode(hash, key, value, null); // 是先插入元素再轉換紅黑樹的 // 鏈長超過8,(那麼實際上已經有9個了)轉換紅黑樹,轉換過程跳過,後面單獨抽出來說 // 特別注意,若是數組長度小於64,不會樹化,是直接擴容 if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st treeifyBin(tab, hash); break; } // 這裏發現有個重複的元素 if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; // 節點移動,這裏差點看不懂了 p = e; } } // existing mapping for key if (e != null) { // 舊值 V oldValue = e.value; // 新值替換舊值,返回舊值 if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); return oldValue; } } // put新元素,除了替換屬於結構改動 // 須要快速失敗 ++modCount; // 桶長超過了 實際元素大小 * 加載因子 // 再散列 if (++size > threshold) resize(); // LinkedHashMap使用的,這裏爲空函數 afterNodeInsertion(evict); // 原來沒有覆蓋舊值是返回null return null; }
// 進行樹化 final void treeifyBin(Node<K,V>[] tab, int hash) { int n, index; Node<K,V> e; // 表空,數組長度小於64進行擴容,不樹化 if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY) resize(); // 當前選擇的桶子不爲空 else if ((e = tab[index = (n - 1) & hash]) != null) { TreeNode<K,V> hd = null, tl = null; // 遍歷鏈表,轉化成樹節點 do { // 雙向鏈表prev,next,輔助性屬性 TreeNode<K,V> p = replacementTreeNode(e, null); if (tl == null) hd = p; else { p.prev = tl; tl.next = p; } tl = p; } while ((e = e.next) != null); // 樹化,節點內部有樹化功能 // 這個樹化是從頭節點開始,即第一個節點當成根節點,而後根節點還沒樹化的鏈表遍歷,一個個插入樹中 // 這個過程穿插着平衡 if ((tab[index] = hd) != null) hd.treeify(tab); } }
TreeNode節點繼承了LinkedHashMap.Entry因此內部屬性好多dom
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> { TreeNode<K,V> parent; // red-black tree links TreeNode<K,V> left; TreeNode<K,V> right; TreeNode<K,V> prev; // needed to unlink next upon deletion boolean red; // next是Node的屬性
3.10 擴容
- 判斷是否初始化過了
- 是:判斷是否達到最大容量
- 設置變量的初值,但還沒進行擴容
- 否:判斷是否設置了加載因子,設置加載因子
- 否:調用默認構造,賦值都是默認的
- 是:判斷是否達到最大容量
final Node<K,V>[] resize() { // 獲取舊錶 Node<K,V>[] oldTab = table; // 獲取舊錶容量 int oldCap = (oldTab == null) ? 0 : oldTab.length; // 舊,新閥值 int oldThr = threshold; int newCap, newThr = 0; // 舊容量大於0,表示初始化過了 if (oldCap > 0) { //若是已經最大容量了,設置閥值整型最大,沒必要要進行再散列了 // 返回舊錶? if (oldCap >= MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; return oldTab; } // 擴容是2倍,和ArrayList的1.5別搞錯了 // 閥值固然也變成2倍了 else if((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >=DEFAULT_INITIAL_CAPACITY) newThr = oldThr << 1; // double threshold } // 這裏和上面對比,說明空表,但舊閥值大於0,說明調用了非空構造函數 else if (oldThr > 0) // initial capacity was placed in threshold newCap = oldThr; // 這裏是容量和閥值都爲0,即調用默認構造函數的結果,即要初始化 else { // zero initial threshold signifies using defaults newCap = DEFAULT_INITIAL_CAPACITY; newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); } if (newThr == 0) { float ft = (float)newCap * loadFactor; newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX_VALUE); } threshold = newThr; @SuppressWarnings({"rawtypes","unchecked"}) // 創建新表 Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; table = newTab; if (oldTab != null) { // 遍歷舊數組每個數組,桶子不爲空才轉移 for (int j = 0; j < oldCap; ++j) { Node<K,V> e; if ((e = oldTab[j]) != null) { oldTab[j] = null; // 只有一個元素,直接插入 if (e.next == null) newTab[e.hash & (newCap - 1)] = e; // 若是是樹 else if (e instanceof TreeNode) ((TreeNode<K,V>)e).split(this, newTab, j, oldCap); else { // preserve order Node<K,V> loHead = null, loTail = null; Node<K,V> hiHead = null, hiTail = null; Node<K,V> next; // 遍歷鏈表 do { next = e.next; // 哈希值 & 舊容量,原位置 if ((e.hash & oldCap) == 0) { if (loTail == null) loHead = e; else loTail.next = e; loTail = e; } // 相對二倍的位置 else { if (hiTail == null) hiHead = e; else hiTail.next = e; hiTail = e; } } while ((e = next) != null); // 這個轉移效率高了,容易看懂 if (loTail != null) { loTail.next = null; newTab[j] = loHead; } if (hiTail != null) { hiTail.next = null; newTab[j + oldCap] = hiHead; } } } } } return newTab; }
// 紅黑樹轉移 final void split(HashMap<K,V> map, Node<K,V>[] tab, int index, int bit) { TreeNode<K,V> b = this; // Relink into lo and hi lists, preserving order TreeNode<K,V> loHead = null, loTail = null; TreeNode<K,V> hiHead = null, hiTail = null; int lc = 0, hc = 0; // 和鏈表差很少,都是遍歷,而後記錄拆分位置 for (TreeNode<K,V> e = b, next; e != null; e = next) { next = (TreeNode<K,V>)e.next; e.next = null; // 分別組裝新舊位置的部分 if ((e.hash & bit) == 0) { if ((e.prev = loTail) == null) loHead = e; else loTail.next = e; loTail = e; ++lc; } else { if ((e.prev = hiTail) == null) hiHead = e; else hiTail.next = e; hiTail = e; ++hc; } } // 而後將這兩部分轉移過去,若是數量不超過‘6’,那麼反樹化 if (loHead != null) { if (lc <= UNTREEIFY_THRESHOLD) // 對這個組裝好的部分,進行反樹化 tab[index] = loHead.untreeify(map); else { // 整個樹都移動過去 tab[index] = loHead; // 若是另外一個位置不爲空,即有轉移到另外一個位置的元素 // 這樣的話,低位就要樹化;若是高位爲空,整個樹都轉移過去了,不用再樹化,原本就是一個樹 if (hiHead != null) loHead.treeify(tab); } } if (hiHead != null) { if (hc <= UNTREEIFY_THRESHOLD) tab[index + bit] = hiHead.untreeify(map); else { tab[index + bit] = hiHead; if (loHead != null) hiHead.treeify(tab); } } }
3.11 ConcurrentHashMap
換成普通的HashMap了,但增長了頭部鎖。桶子上的節點用CAS操做完成,而鏈表或紅黑樹裏面的用Synchronizedssh
3.11.1 原理
// 是普通的1.8哈希表 // 不一樣在於,每次修改結構會鎖住 鏈表的頭,紅黑樹的根 // Synchronized(Node / Root)
3.11.2 put方法
final V putVal(K key, V value, boolean onlyIfAbsent) { if (key == null || value == null) throw new NullPointerException(); int hash = spread(key.hashCode()); int binCount = 0; for (Node<K,V>[] tab = table;;) { Node<K,V> f; int n, i, fh; // 初始化表 if (tab == null || (n = tab.length) == 0) tab = initTable(); // 第一個是否爲空,Unsafe類直接獲取內存值 else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) { // no lock when adding to empty bin // CAS操做 if (casTabAt(tab, i, null,new Node<K,V>(hash, key, value, null))) break; } // MOVDE = -1,表示整個數組在擴容 else if ((fh = f.hash) == MOVED) // 表示當前線程也去幫忙擴容 tab = helpTransfer(tab, f); else { V oldVal = null; // 竟然用了synchronized,鎖住了頭節點,意外 synchronized (f) { // 若加鎖時,另外線程刪除了這個節點,鎖沒了 // 那麼會再次循環,而後獲取最新的頭節點 if (tabAt(tab, i) == f) { // 遍歷鏈表 if (fh >= 0) { binCount = 1; for (Node<K,V> e = f;; ++binCount) { K ek; if (e.hash == hash && ((ek = e.key) == key || (ek != null && key.equals(ek)))) { oldVal = e.val; if (!onlyIfAbsent) e.val = value; break; } Node<K,V> pred = e; if ((e = e.next) == null) { pred.next = new Node<K,V>(hash, key, value, null); break; } } } else if (f instanceof TreeBin) { Node<K,V> p; binCount = 2; if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key, value)) != null) { oldVal = p.val; if (!onlyIfAbsent) p.val = value; } } } } // 樹化內部有鎖 if (binCount != 0) { if (binCount >= TREEIFY_THRESHOLD) treeifyBin(tab, i); if (oldVal != null) return oldVal; break; } } } // 這裏纔是真正的擴容,上面的helpTransfer表示幫助擴容 addCount(1L, binCount); return null; }
3.11.3 init初始化
private final Node<K,V>[] initTable() { Node<K,V>[] tab; int sc; while ((tab = table) == null || tab.length == 0) { if ((sc = sizeCtl) < 0) // 有可能一直競爭到時間片??? 不太可能把 Thread.yield(); // lost initialization race; just spin // CAS改變了sc轉檯 else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) { try { if ((tab = table) == null || tab.length == 0) { int n = (sc > 0) ? sc : DEFAULT_CAPACITY; @SuppressWarnings("unchecked") Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n]; table = tab = nt; sc = n - (n >>> 2); } } finally { sizeCtl = sc; } break; } } return tab; }
3.11.4 擴容
private final void addCount(long x, int check) { CounterCell[] as; long b, s; // 邏輯 ||,兩個條件有條件執不執行 if ((as = counterCells) != null || !U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) { CounterCell a; long v; int m; boolean uncontended = true; if (as == null || (m = as.length - 1) < 0 || (a = as[ThreadLocalRandom.getProbe() & m]) == null || !(uncontended = U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) { fullAddCount(x, uncontended); return; } if (check <= 1) return; s = sumCount(); } if (check >= 0) { Node<K,V>[] tab, nt; int n, sc; while (s >= (long)(sc = sizeCtl) && (tab = table) != null && (n = tab.length) < MAXIMUM_CAPACITY) { int rs = resizeStamp(n); if (sc < 0) { if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 || sc == rs + MAX_RESIZERS || (nt = nextTable) == null || transferIndex <= 0) break; if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)) transfer(tab, nt); } else if (U.compareAndSwapInt(this, SIZECTL, sc, (rs << RESIZE_STAMP_SHIFT) + 2)) transfer(tab, null); s = sumCount(); } } }
3.11.5 普通方法
public int size() { long n = sumCount(); return ((n < 0L) ? 0 : (n > (long)Integer.MAX_VALUE) ? Integer.MAX_VALUE : (int)n); }
final long sumCount() { CounterCell[] as = counterCells; CounterCell a; long sum = baseCount; if (as != null) { for (int i = 0; i < as.length; ++i) { if ((a = as[i]) != null) sum += a.value; } } return sum; }
4. 總結
4.1 兩個版本的區別
- 1.8 添加了紅黑樹(logN)
- 1.7先擴容再添加元素,但空桶不擴容、1.8先添加元素再擴容
- 1.7和1.8的2次冪方法不一樣
- 1.7擴容是一個一個轉、1.8是分紅兩個部分組裝好了,最後才轉移過去
- 1.7有單獨的null方法,而1.8沒有使用==判斷了
- 1.8擴容,當數組長度<64時,優先擴容,不先轉化紅黑樹
4.2 併發區別
- 1.7 使用分段鎖,ReentranLock、CAS的使用,使用雙重數組
- 1.8 使用頭部鎖,Synchronized、CAS的使用,使用普通哈希表
本文同步分享在 博客「Howlet」(CNBlog)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。