分析1.7、1.8的HashMap、ConcurrentHashMap的區別
越到後面越煩躁,寫不下去了,到時回來填坑把
1. 補充位運算
位運算是對2進位制而言的
符號 | 描述 | 運算規則 |
---|---|---|
& | 與 | 兩個都為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,下面看原理
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所以內部屬性好多
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操作完成,而連結串列或紅黑樹裡面的用Synchronized
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的使用,使用普通雜湊表