Java7 HashMapjava
HashMap 是最簡單的,一來咱們很是熟悉,二來就是它不支持併發操做,因此源碼也很是簡單。node
首先,咱們用下面這張圖來介紹 HashMap 的結構。數組
這個僅僅是示意圖,由於沒有考慮到數組要擴容的狀況,具體的後面再說。安全
大方向上,HashMap 裏面是一個數組,而後數組中每一個元素是一個單向鏈表。併發
上圖中,每一個綠色的實體是嵌套類 Entry 的實例,Entry 包含四個屬性:key, value, hash 值和用於單向鏈表的 next。ssh
capacity:當前數組容量,始終保持 2^n,能夠擴容,擴容後數組大小爲當前的 2 倍。函數
loadFactor:負載因子,默認爲 0.75。oop
threshold:擴容的閾值,等於 capacity * loadFactorpost
put 過程分析ui
仍是比較簡單的,跟着代碼走一遍吧。
public V put(K key, V value) {
// 當插入第一個元素的時候,須要先初始化數組大小
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
// 若是 key 爲 null,感興趣的能夠往裏看,最終會將這個 entry 放到 table[0] 中
if (key == null)
return putForNullKey(value);
// 1. 求 key 的 hash 值
int hash = hash(key);
// 2. 找到對應的數組下標
int i = indexFor(hash, table.length);
// 3. 遍歷一下對應下標處的鏈表,看是否有重複的 key 已經存在,
// 若是有,直接覆蓋,put 方法返回舊值就結束了
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
// 4. 不存在重複的 key,將此 entry 添加到鏈表中,細節後面說
addEntry(hash, key, value, i);
return null;
}
數組初始化
在第一個元素插入 HashMap 的時候作一次數組的初始化,就是先肯定初始的數組大小,並計算數組擴容的閾值。
private void inflateTable(int toSize) {
// 保證數組大小必定是 2 的 n 次方。
// 好比這樣初始化:new HashMap(20),那麼處理成初始數組大小是 32
int capacity = roundUpToPowerOf2(toSize);
// 計算擴容閾值:capacity * loadFactor
threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
// 算是初始化數組吧
table = new Entry[capacity];
initHashSeedAsNeeded(capacity); //ignore
}
這裏有一個將數組大小保持爲 2 的 n 次方的作法,Java7 和 Java8 的 HashMap 和 ConcurrentHashMap 都有相應的要求,只不過實現的代碼稍微有些不一樣,後面再看到的時候就知道了。
計算具體數組位置
這個簡單,咱們本身也能 YY 一個:使用 key 的 hash 值對數組長度進行取模就能夠了。
static int indexFor(int hash, int length) {
// assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";
return hash & (length-1);
}
這個方法很簡單,簡單說就是取 hash 值的低 n 位。如在數組長度爲 32 的時候,其實取的就是 key 的 hash 值的低 5 位,做爲它在數組中的下標位置。
添加節點到鏈表中
找到數組下標後,會先進行 key 判重,若是沒有重複,就準備將新值放入到鏈表的表頭。
void addEntry(int hash, K key, V value, int bucketIndex) {
// 若是當前 HashMap 大小已經達到了閾值,而且新值要插入的數組位置已經有元素了,那麼要擴容
if ((size >= threshold) && (null != table[bucketIndex])) {
// 擴容,後面會介紹一下
resize(2 * table.length);
// 擴容之後,從新計算 hash 值
hash = (null != key) ? hash(key) : 0;
// 從新計算擴容後的新的下標
bucketIndex = indexFor(hash, table.length);
}
// 往下看
createEntry(hash, key, value, bucketIndex);
}
// 這個很簡單,其實就是將新值放到鏈表的表頭,而後 size++
void createEntry(int hash, K key, V value, int bucketIndex) {
Entry<K,V> e = table[bucketIndex];
table[bucketIndex] = new Entry<>(hash, key, value, e);
size++;
}
這個方法的主要邏輯就是先判斷是否須要擴容,須要的話先擴容,而後再將這個新的數據插入到擴容後的數組的相應位置處的鏈表的表頭。
數組擴容
前面咱們看到,在插入新值的時候,若是當前的 size 已經達到了閾值,而且要插入的數組位置上已經有元素,那麼就會觸發擴容,擴容後,數組大小爲原來的 2 倍。
void resize(int newCapacity) {
Entry[] oldTable = table;
int oldCapacity = oldTable.length;
if (oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return;
}
// 新的數組
Entry[] newTable = new Entry[newCapacity];
// 將原來數組中的值遷移到新的更大的數組中
transfer(newTable, initHashSeedAsNeeded(newCapacity));
table = newTable;
threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}
擴容就是用一個新的大數組替換原來的小數組,並將原來數組中的值遷移到新的數組中。
因爲是雙倍擴容,遷移過程當中,會將原來 table[i] 中的鏈表的全部節點,分拆到新的數組的 newTable[i] 和 newTable[i + oldLength] 位置上。如原來數組長度是 16,那麼擴容後,原來 table[0] 處的鏈表中的全部元素會被分配到新數組中 newTable[0] 和 newTable[16] 這兩個位置。代碼比較簡單,這裏就不展開了。
get 過程分析
相對於 put 過程,get 過程是很是簡單的。
根據 key 計算 hash 值。
找到相應的數組下標:hash & (length – 1)。
遍歷該數組位置處的鏈表,直到找到相等(==或equals)的 key。
public V get(Object key) {
// 以前說過,key 爲 null 的話,會被放到 table[0],因此只要遍歷下 table[0] 處的鏈表就能夠了
if (key == null)
return getForNullKey();
//
Entry<K,V> entry = getEntry(key);
return null == entry ? null : entry.getValue();
}
getEntry(key):
final Entry<K,V> getEntry(Object key) {
if (size == 0) {
return null;
}
int hash = (key == null) ? 0 : hash(key);
// 肯定數組下標,而後從頭開始遍歷鏈表,直到找到爲止
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;
}
Java7 ConcurrentHashMap
ConcurrentHashMap 和 HashMap 思路是差很少的,可是由於它支持併發操做,因此要複雜一些。
整個 ConcurrentHashMap 由一個個 Segment 組成,Segment 表明」部分「或」一段「的意思,因此不少地方都會將其描述爲分段鎖。注意,行文中,我不少地方用了「槽」來表明一個 segment。
簡單理解就是,ConcurrentHashMap 是一個 Segment 數組,Segment 經過繼承 ReentrantLock 來進行加鎖,因此每次須要加鎖的操做鎖住的是一個 segment,這樣只要保證每一個 Segment 是線程安全的,也就實現了全局的線程安全。
concurrencyLevel:並行級別、併發數、Segment 數,怎麼翻譯不重要,理解它。默認是 16,也就是說 ConcurrentHashMap 有 16 個 Segments,因此理論上,這個時候,最多能夠同時支持 16 個線程併發寫,只要它們的操做分別分佈在不一樣的 Segment 上。這個值能夠在初始化的時候設置爲其餘值,可是一旦初始化之後,它是不能夠擴容的。
再具體到每一個 Segment 內部,其實每一個 Segment 很像以前介紹的 HashMap,不過它要保證線程安全,因此處理起來要麻煩些。
初始化
initialCapacity:初始容量,這個值指的是整個 ConcurrentHashMap 的初始容量,實際操做的時候須要平均分給每一個 Segment。
loadFactor:負載因子,以前咱們說了,Segment 數組不能夠擴容,因此這個負載因子是給每一個 Segment 內部使用的。
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;
// 計算並行級別 ssize,由於要保持並行級別是 2 的 n 次方
while (ssize < concurrencyLevel) {
++sshift;
ssize <<= 1;
}
// 咱們這裏先不要那麼燒腦,用默認值,concurrencyLevel 爲 16,sshift 爲 4
// 那麼計算出 segmentShift 爲 28,segmentMask 爲 15,後面會用到這兩個值
this.segmentShift = 32 - sshift;
this.segmentMask = ssize - 1;
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
// initialCapacity 是設置整個 map 初始的大小,
// 這裏根據 initialCapacity 計算 Segment 數組中每一個位置能夠分到的大小
// 如 initialCapacity 爲 64,那麼每一個 Segment 或稱之爲"槽"能夠分到 4 個
int c = initialCapacity / ssize;
if (c * ssize < initialCapacity)
++c;
// 默認 MIN_SEGMENT_TABLE_CAPACITY 是 2,這個值也是有講究的,由於這樣的話,對於具體的槽上,
// 插入一個元素不至於擴容,插入第二個的時候纔會擴容
int cap = MIN_SEGMENT_TABLE_CAPACITY;
while (cap < c)
cap <<= 1;
// 建立 Segment 數組,
// 並建立數組的第一個元素 segment[0]
Segment<K,V> s0 =
new Segment<K,V>(loadFactor, (int)(cap * loadFactor),
(HashEntry<K,V>[])new HashEntry[cap]);
Segment<K,V>[] ss = (Segment<K,V>[])new Segment[ssize];
// 往數組寫入 segment[0]
UNSAFE.putOrderedObject(ss, SBASE, s0); // ordered write of segments[0]
this.segments = ss;
}
初始化完成,咱們獲得了一個 Segment 數組。
咱們就當是用 new ConcurrentHashMap() 無參構造函數進行初始化的,那麼初始化完成後:
Segment 數組長度爲 16,不能夠擴容
Segment[i] 的默認大小爲 2,負載因子是 0.75,得出初始閾值爲 1.5,也就是之後插入第一個元素不會觸發擴容,插入第二個會進行第一次擴容
這裏初始化了 segment[0],其餘位置仍是 null,至於爲何要初始化 segment[0],後面的代碼會介紹
當前 segmentShift 的值爲 32 – 4 = 28,segmentMask 爲 16 – 1 = 15,姑且把它們簡單翻譯爲移位數和掩碼,這兩個值立刻就會用到
put 過程分析
咱們先看 put 的主流程,對於其中的一些關鍵細節操做,後面會進行詳細介紹。
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);
}
第一層皮很簡單,根據 hash 值很快就能找到相應的 Segment,以後就是 Segment 內部的 put 操做了。
Segment 內部是由 數組+鏈表 組成的。
final V put(K key, int hash, V value, boolean onlyIfAbsent) {
// 在往該 segment 寫入前,須要先獲取該 segment 的獨佔鎖
// 先看主流程,後面還會具體介紹這部份內容
HashEntry<K,V> node = tryLock() ? null :
scanAndLockForPut(key, hash, value);
V oldValue;
try {
// 這個是 segment 內部的數組
HashEntry<K,V>[] tab = table;
// 再利用 hash 值,求應該放置的數組下標
int index = (tab.length - 1) & hash;
// first 是數組該位置處的鏈表的表頭
HashEntry<K,V> first = entryAt(tab, index);
// 下面這串 for 循環雖然很長,不過也很好理解,想一想該位置沒有任何元素和已經存在一個鏈表這兩種狀況
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 {
// node 究竟是不是 null,這個要看獲取鎖的過程,不過和這裏都沒有關係。
// 若是不爲 null,那就直接將它設置爲鏈表表頭;若是是null,初始化並設置爲鏈表表頭。
if (node != null)
node.setNext(first);
else
node = new HashEntry<K,V>(hash, key, value, first);
int c = count + 1;
// 若是超過了該 segment 的閾值,這個 segment 須要擴容
if (c > threshold && tab.length < MAXIMUM_CAPACITY)
rehash(node); // 擴容後面也會具體分析
else
// 沒有達到閾值,將 node 放到數組 tab 的 index 位置,
// 其實就是將新的節點設置成原鏈表的表頭
setEntryAt(tab, index, node);
++modCount;
count = c;
oldValue = null;
break;
}
}
} finally {
// 解鎖
unlock();
}
return oldValue;
}
總體流程仍是比較簡單的,因爲有獨佔鎖的保護,因此 segment 內部的操做並不複雜。至於這裏面的併發問題,咱們稍後再進行介紹。
到這裏 put 操做就結束了,接下來,咱們說一說其中幾步關鍵的操做。
初始化槽: ensureSegment
ConcurrentHashMap 初始化的時候會初始化第一個槽 segment[0],對於其餘槽來講,在插入第一個值的時候進行初始化。
這裏須要考慮併發,由於極可能會有多個線程同時進來初始化同一個槽 segment[k],不過只要有一個成功了就能夠。
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;
if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u)) == null) {
// 這裏看到爲何以前要初始化 segment[0] 了,
// 使用當前 segment[0] 處的數組長度和負載因子來初始化 segment[k]
// 爲何要用「當前」,由於 segment[0] 可能早就擴容過了
Segment<K,V> proto = ss[0];
int cap = proto.table.length;
float lf = proto.loadFactor;
int threshold = (int)(cap * lf);
// 初始化 segment[k] 內部的數組
HashEntry<K,V>[] tab = (HashEntry<K,V>[])new HashEntry[cap];
if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))
== null) { // 再次檢查一遍該槽是否被其餘線程初始化了。
Segment<K,V> s = new Segment<K,V>(lf, threshold, tab);
// 使用 while 循環,內部用 CAS,當前線程成功設值或其餘線程成功設值後,退出
while ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))
== null) {
if (UNSAFE.compareAndSwapObject(ss, u, null, seg = s))
break;
}
}
}
return seg;
}
總的來講,ensureSegment(int k) 比較簡單,對於併發操做使用 CAS 進行控制。
我沒搞懂這裏爲何要搞一個 while 循環,CAS 失敗不就表明有其餘線程成功了嗎,爲何要再進行判斷?
獲取寫入鎖: scanAndLockForPut
前面咱們看到,在往某個 segment 中 put 的時候,首先會調用 node = tryLock() ? null : scanAndLockForPut(key, hash, value),也就是說先進行一次 tryLock() 快速獲取該 segment 的獨佔鎖,若是失敗,那麼進入到 scanAndLockForPut 這個方法來獲取鎖。
下面咱們來具體分析這個方法中是怎麼控制加鎖的。
private HashEntry<K,V> scanAndLockForPut(K key, int hash, V value) {
HashEntry<K,V> first = entryForHash(this, hash);
HashEntry<K,V> e = first;
HashEntry<K,V> node = null;
int retries = -1; // negative while locating node
// 循環獲取鎖
while (!tryLock()) {
HashEntry<K,V> f; // to recheck first below
if (retries < 0) {
if (e == null) {
if (node == null) // speculatively create node
// 進到這裏說明數組該位置的鏈表是空的,沒有任何元素
// 固然,進到這裏的另外一個緣由是 tryLock() 失敗,因此該槽存在併發,不必定是該位置
node = new HashEntry<K,V>(hash, key, value, null);
retries = 0;
}
else if (key.equals(e.key))
retries = 0;
else
// 順着鏈表往下走
e = e.next;
}
// 重試次數若是超過 MAX_SCAN_RETRIES(單核1多核64),那麼不搶了,進入到阻塞隊列等待鎖
// lock() 是阻塞方法,直到獲取鎖後返回
else if (++retries > MAX_SCAN_RETRIES) {
lock();
break;
}
else if ((retries & 1) == 0 &&
// 這個時候是有大問題了,那就是有新的元素進到了鏈表,成爲了新的表頭
// 因此這邊的策略是,至關於從新走一遍這個 scanAndLockForPut 方法
(f = entryForHash(this, hash)) != first) {
e = first = f; // re-traverse if entry changed
retries = -1;
}
}
return node;
}
這個方法有兩個出口,一個是 tryLock() 成功了,循環終止,另外一個就是重試次數超過了 MAX_SCAN_RETRIES,進到 lock() 方法,此方法會阻塞等待,直到成功拿到獨佔鎖。
這個方法就是看似複雜,可是其實就是作了一件事,那就是獲取該 segment 的獨佔鎖,若是須要的話順便實例化了一下 node。
擴容: rehash
重複一下,segment 數組不能擴容,擴容是 segment 數組某個位置內部的數組 HashEntry\[] 進行擴容,擴容後,容量爲原來的 2 倍。
首先,咱們要回顧一下觸發擴容的地方,put 的時候,若是判斷該值的插入會致使該 segment 的元素個數超過閾值,那麼先進行擴容,再插值,讀者這個時候能夠回去 put 方法看一眼。
該方法不須要考慮併發,由於到這裏的時候,是持有該 segment 的獨佔鎖的。
// 方法參數上的 node 是此次擴容後,須要添加到新的數組中的數據。
private void rehash(HashEntry<K,V> node) {
HashEntry<K,V>[] oldTable = table;
int oldCapacity = oldTable.length;
// 2 倍
int newCapacity = oldCapacity << 1;
threshold = (int)(newCapacity * loadFactor);
// 建立新數組
HashEntry<K,V>[] newTable =
(HashEntry<K,V>[]) new HashEntry[newCapacity];
// 新的掩碼,如從 16 擴容到 32,那麼 sizeMask 爲 31,對應二進制 ‘000...00011111’
int sizeMask = newCapacity - 1;
// 遍歷原數組,老套路,將原數組位置 i 處的鏈表拆分到 新數組位置 i 和 i+oldCap 兩個位置
for (int i = 0; i < oldCapacity ; i++) {
// e 是鏈表的第一個元素
HashEntry<K,V> e = oldTable[i];
if (e != null) {
HashEntry<K,V> next = e.next;
// 計算應該放置在新數組中的位置,
// 假設原數組長度爲 16,e 在 oldTable[3] 處,那麼 idx 只多是 3 或者是 3 + 16 = 19
int idx = e.hash & sizeMask;
if (next == null) // 該位置處只有一個元素,那比較好辦
newTable[idx] = e;
else { // Reuse consecutive sequence at same slot
// e 是鏈表表頭
HashEntry<K,V> lastRun = e;
// idx 是當前鏈表的頭結點 e 的新位置
int lastIdx = idx;
// 下面這個 for 循環會找到一個 lastRun 節點,這個節點以後的全部元素是將要放到一塊兒的
for (HashEntry<K,V> last = next;
last != null;
last = last.next) {
int k = last.hash & sizeMask;
if (k != lastIdx) {
lastIdx = k;
lastRun = last;
}
}
// 將 lastRun 及其以後的全部節點組成的這個鏈表放到 lastIdx 這個位置
newTable[lastIdx] = lastRun;
// 下面的操做是處理 lastRun 以前的節點,
// 這些節點可能分配在另外一個鏈表中,也可能分配到上面的那個鏈表中
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);
}
}
}
}
// 將新來的 node 放到新數組中剛剛的 兩個鏈表之一 的 頭部
int nodeIndex = node.hash & sizeMask; // add the new node
node.setNext(newTable[nodeIndex]);
newTable[nodeIndex] = node;
table = newTable;
}
這裏的擴容比以前的 HashMap 要複雜一些,代碼難懂一點。上面有兩個挨着的 for 循環,第一個 for 有什麼用呢?
仔細一看發現,若是沒有第一個 for 循環,也是能夠工做的,可是,這個 for 循環下來,若是 lastRun 的後面還有比較多的節點,那麼此次就是值得的。由於咱們只須要克隆 lastRun 前面的節點,後面的一串節點跟着 lastRun 走就是了,不須要作任何操做。
我以爲 Doug Lea 的這個想法也是挺有意思的,不過比較壞的狀況就是每次 lastRun 都是鏈表的最後一個元素或者很靠後的元素,那麼此次遍歷就有點浪費了。不過 Doug Lea 也說了,根據統計,若是使用默認的閾值,大約只有 1/6 的節點須要克隆。
get 過程分析
相對於 put 來講,get 真的不要太簡單。
計算 hash 值,找到 segment 數組中的具體位置,或咱們前面用的「槽」
槽中也是一個數組,根據 hash 找到數組中具體的位置
到這裏是鏈表了,順着鏈表進行查找便可
public V get(Object key) {
Segment<K,V> s; // manually integrate access methods to reduce overhead
HashEntry<K,V>[] tab;
// 1. hash 值
int h = hash(key);
long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE;
// 2. 根據 hash 找到對應的 segment
if ((s = (Segment<K,V>)UNSAFE.getObjectVolatile(segments, u)) != null &&
(tab = s.table) != null) {
// 3. 找到segment 內部數組相應位置的鏈表,遍歷
for (HashEntry<K,V> e = (HashEntry<K,V>) UNSAFE.getObjectVolatile
(tab, ((long)(((tab.length - 1) & h)) << TSHIFT) + TBASE);
e != null; e = e.next) {
K k;
if ((k = e.key) == key || (e.hash == h && key.equals(k)))
return e.value;
}
}
return null;
}
併發問題分析
如今咱們已經說完了 put 過程和 get 過程,咱們能夠看到 get 過程當中是沒有加鎖的,那天然咱們就須要去考慮併發問題。
添加節點的操做 put 和刪除節點的操做 remove 都是要加 segment 上的獨佔鎖的,因此它們之間天然不會有問題,咱們須要考慮的問題就是 get 的時候在同一個 segment 中發生了 put 或 remove 操做。
1. put 操做的線程安全性。
初始化槽,這個咱們以前就說過了,使用了 CAS 來初始化 Segment 中的數組。
添加節點到鏈表的操做是插入到表頭的,因此,若是這個時候 get 操做在鏈表遍歷的過程已經到了中間,是不會影響的。固然,另外一個併發問題就是 get 操做在 put 以後,須要保證剛剛插入表頭的節點被讀取,這個依賴於 setEntryAt 方法中使用的 UNSAFE.putOrderedObject。
擴容。擴容是新建立了數組,而後進行遷移數據,最後面將 newTable 設置給屬性 table。因此,若是 get 操做此時也在進行,那麼也不要緊,若是 get 先行,那麼就是在舊的 table 上作查詢操做;而 put 先行,那麼 put 操做的可見性保證就是 table 使用了 volatile 關鍵字。
2. remove 操做的線程安全性。
remove 操做咱們沒有分析源碼,因此這裏說的讀者感興趣的話仍是須要到源碼中去求實一下的。
get 操做須要遍歷鏈表,可是 remove 操做會」破壞」鏈表。
若是 remove 破壞的節點 get 操做已通過去了,那麼這裏不存在任何問題。
若是 remove 先破壞了一個節點,分兩種狀況考慮。 一、若是此節點是頭結點,那麼須要將頭結點的 next 設置爲數組該位置的元素,table 雖然使用了 volatile 修飾,可是 volatile 並不能提供數組內部操做的可見性保證,因此源碼中使用了 UNSAFE 來操做數組,請看方法 setEntryAt。二、若是要刪除的節點不是頭結點,它會將要刪除節點的後繼節點接到前驅節點中,這裏的併發保證就是 next 屬性是 volatile 的。
Java8 HashMap
Java8 對 HashMap 進行了一些修改,最大的不一樣就是利用了紅黑樹,因此其由 數組+鏈表+紅黑樹 組成。
根據 Java7 HashMap 的介紹,咱們知道,查找的時候,根據 hash 值咱們可以快速定位到數組的具體下標,可是以後的話,須要順着鏈表一個個比較下去才能找到咱們須要的,時間複雜度取決於鏈表的長度,爲 O(n)。
爲了下降這部分的開銷,在 Java8 中,當鏈表中的元素超過了 8 個之後,會將鏈表轉換爲紅黑樹,在這些位置進行查找的時候能夠下降時間複雜度爲 O(logN)。
來一張圖簡單示意一下吧:
注意,上圖是示意圖,主要是描述結構,不會達到這個狀態的,由於這麼多數據的時候早就擴容了。
下面,咱們仍是用代碼來介紹吧,我的感受,Java8 的源碼可讀性要差一些,不過精簡一些。
Java7 中使用 Entry 來表明每一個 HashMap 中的數據節點,Java8 中使用 Node,基本沒有區別,都是 key,value,hash 和 next 這四個屬性,不過,Node 只能用於鏈表的狀況,紅黑樹的狀況須要使用 TreeNode。
咱們根據數組元素中,第一個節點數據類型是 Node 仍是 TreeNode 來判斷該位置下是鏈表仍是紅黑樹的。
put 過程分析
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
// 第三個參數 onlyIfAbsent 若是是 true,那麼只有在不存在該 key 時纔會進行 put 操做
// 第四個參數 evict 咱們這裏不關心
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
// 第一次 put 值的時候,會觸發下面的 resize(),相似 java7 的第一次 put 也要初始化數組長度
// 第一次 resize 和後續的擴容有些不同,由於此次是數組從 null 初始化到默認的 16 或自定義的初始容量
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// 找到具體的數組下標,若是此位置沒有值,那麼直接初始化一下 Node 並放置在這個位置就能夠了
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {// 數組該位置有數據
Node<K,V> e; K k;
// 首先,判斷該位置的第一個數據和咱們要插入的數據,key 是否是"相等",若是是,取出這個節點
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) {
// 插入到鏈表的最後面(Java7 是插入到鏈表的最前面)
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
// TREEIFY_THRESHOLD 爲 8,因此,若是新插入的值是鏈表中的第 9 個
// 會觸發下面的 treeifyBin,也就是將鏈表轉換爲紅黑樹
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
// 若是在該鏈表中找到了"相等"的 key(== 或 equals)
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
// 此時 break,那麼 e 爲鏈表中[與要插入的新值的 key "相等"]的 node
break;
p = e;
}
}
// e!=null 說明存在舊值的key與要插入的key"相等"
// 對於咱們分析的put操做,下面這個 if 其實就是進行 "值覆蓋",而後返回舊值
if (e != null) {
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
// 若是 HashMap 因爲新插入這個值致使 size 已經超過了閾值,須要進行擴容
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
和 Java7 稍微有點不同的地方就是,Java7 是先擴容後插入新值的,Java8 先插值再擴容,不過這個不重要。
數組擴容
resize() 方法用於初始化數組或數組擴容,每次擴容後,容量爲原來的 2 倍,並進行數據遷移。
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
if (oldCap > 0) { // 對應數組擴容
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
// 將數組大小擴大一倍
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
// 將閾值擴大一倍
newThr = oldThr << 1; // double threshold
}
else if (oldThr > 0) // 對應使用 new HashMap(int initialCapacity) 初始化後,第一次 put 的時候
newCap = oldThr;
else {// 對應使用 new HashMap() 初始化後,第一次 put 的時候
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;
// 用新的數組大小初始化新的數組
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab; // 若是是初始化數組,到這裏就結束了,返回 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 {
// 這塊是處理鏈表的狀況,
// 須要將此鏈表拆成兩個鏈表,放到新的數組中,而且保留原來的前後順序
// loHead、loTail 對應一條鏈表,hiHead、hiTail 對應另外一條鏈表,代碼仍是比較簡單的
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;
// 第二條鏈表的新的位置是 j + oldCap,這個很好理解
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
get 過程分析
相對於 put 來講,get 真的太簡單了。
計算 key 的 hash 值,根據 hash 值找到對應數組下標: hash & (length-1)
判斷數組該位置處的元素是否恰好就是咱們要找的,若是不是,走第三步
判斷該元素類型是不是 TreeNode,若是是,用紅黑樹的方法取數據,若是不是,走第四步
遍歷鏈表,直到找到相等(==或equals)的 key
public V get(Object key) {
Node<K,V> e;
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;
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
// 判斷第一個節點是否是就是須要的
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
if ((e = first.next) != null) {
// 判斷是不是紅黑樹
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
// 鏈表遍歷
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}