數據結構:html
數組 + 單鏈表node
transient Entry[] table; // 數組 static class Entry<K,V> implements Map.Entry<K,V> { final K key; V value; Entry<K,V> next;// 單鏈表,存儲hash衝突的對象 final int hash;
hash桶的計算:算法
首先把hash桶的個數適配到2的n次方shell
int capacity = 1; while (capacity < initialCapacity) capacity <<= 1;//把容量適配到2^n,方便後面的 this.loadFactor = loadFactor; threshold = (int)(capacity * loadFactor); table = new Entry[capacity];
hash桶的查找經過hashcode與(桶個數-1)進行按位與操做,至關於取模,可是效率要高。數組
static int indexFor(int h, int length) { return h & (length-1); }
爲何要把hashmap的容量適配到2的n次方呢?由於2^n-1正好各個位都是1,這樣在按位與操做時其結果徹底取決於hashcode,只要hashcode算法得當,就可使得hash桶的數據分佈比較均勻。若是容量不是2的n次方的話,就會出現0的位,會致使進行與操做後有些桶就一直放不進數據的狀況。安全
旋轉hash:在原有hashcode的基礎上再hash一次,充分利用高位進行計算,減小因低位相同的狀況致使的hash碰撞。數據結構
static int hash(int h) { h ^= (h >>> 20) ^ (h >>> 12); return h ^ (h >>> 7) ^ (h >>> 4); }
擴容:併發
當數據量達到閥值(capacity * loadFactor)時,爲了減小數據量增長帶來的hash碰撞,須要對HashMap進行擴容。須要把全部的entry移動一次,代價較大,因此在能夠預估容量的時候儘可能在初始化時指定容量。高併發
void transfer(Entry[] newTable) { Entry[] src = table; int newCapacity = newTable.length; for (int j = 0; j < src.length; j++) { Entry<K,V> e = src[j]; if (e != null) { src[j] = null; do { Entry<K,V> next = e.next; int i = indexFor(e.hash, newCapacity); e.next = newTable[i]; newTable[i] = e; e = next; } while (e != null); } } }
fail-fast:性能
HashMap是非線程安全的集合,在進行HashMap遍歷(HashIterator)操做時,若是map有修改操做,都會增長modCount的值,經過modCount與expectModCount進行比較,若是二者不相等,當即拋出ConcurrentModificationException。
數據結構:
數組 + 單鏈表 + 雙向鏈表
LinkedHashMap是在HashMap的基礎上添加了一個雙向鏈表結構,來按必定的順序維護裏面的全部entry。
Entry<K,V> before, after;//雙向鏈表結構 header = new Entry<K,V>(-1, null, null, null); header.before = header.after = header;
順序性:
提供兩種順序方式來維護entry:
按插入有序:Insertion-Ordered,全部entry按插入的順序排序,讀取的時候老是從最早插入的那個entry開始讀取。
按訪問有序:Access-Ordered(全部entry從least-recently到most-recently再到header排列)。entry每次被訪問都要調整它的順序,從新放到header結點前面,這樣一直不被訪問的entry就離header愈來愈遠。
這是怎麼作到的呢?LinkedHashMap的添加結點操做都是addBefore,並且每次都是在header結點以前進行插入(其實這裏面的head結點是個尾結點)離尾結點最遠的就是最老的結點(header.after指向),這是個逆向鏈,因此遍歷的時候從header.after開始,就能取到按插入有序的數據。
private void addBefore(Entry<K,V> existingEntry) { after = existingEntry; before = existingEntry.before; before.after = this; after.before = this; }
遍歷操做,從header.after開始遍歷。
Entry<K,V> nextEntry = header.after; Entry<K,V> nextEntry() { if (modCount != expectedModCount) throw new ConcurrentModificationException(); if (nextEntry == header) throw new NoSuchElementException(); Entry<K,V> e = lastReturned = nextEntry; nextEntry = e.after; return e; }
LRU
LinkedHashMap自然支持LRU操做,即Access-Ordered,默認不開啓。
protected boolean removeEldestEntry(Map.Entry eldest) { return false;// 默認是返回false,咱們能夠實現這個方法來支持LRU }
其餘
非線程安全,fast-fail(同hashmap)
ConcurrentHashMap採用了鎖分離的技術來實現確保線程安全的狀況下達到較好的性能。它把整個hash table分紅好多個小的hash table(即Segment),每一個Segment都有本身的鎖來保證線程安全,這樣就使得各個Segment均可以獨立地進行管理,而不須要爭用鎖。
兩個重要的結構:HashEntry和Segment
HashEntry中,value被申明爲volatile,這樣保證了value的可見性,併發訪問時不會出現髒數據。next被申請爲final,保證了鏈表的中間和結尾部分都不會改變,進行讀操做時就不須要加鎖,這樣能夠提升併發性。
final K key; final int hash; volatile V value; final HashEntry<K,V> next; // 下面這句是put操做的行爲,也就是每次put都是往頭節點前面插入新節點,不影響原來的鏈表結構。 tab[index] = new HashEntry<K,V>(key, hash, first, value);
Segment繼承了ReentrantLock,天生具備鎖的功能,因此在put或remove操做時能夠直接加鎖使用。
擴容(rehash)操做
若是原hash桶的鏈表裏的全部結點rehash值都同樣,直接把鏈表鏈接到新桶上便可;
不然就找到鏈表尾部相同rehash值的子鏈表,直接鏈接到新桶上(代碼片斷一),這樣保證rehash到同一個桶的多個節點不會出現連接順序反轉的狀況,也避免了像HashMap那樣在高併發下rehash出現死循環的現象(http://coolshell.cn/articles/9606.html)。
最後把當前子鏈表前面的那部分節點正常計算rehash並添加到新桶的位置(代碼片斷二)。
// 代碼片斷一 // Reuse trailing 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;
這裏解釋一下上面這行代碼newTable[lastIdx] = lastRun
, 剛開始在懷疑直接給新桶賦值的話會不會被後面的entry給覆蓋掉?仔細想一想,徹底不必擔憂這個。舉個例子,segment(小hash表)容量從32擴充到64的狀況,也就是容量從2^5=100000(掩碼是:011111)擴充到2^6=1000000(掩碼是:[1]11111)這個[]裏的1就是擴充後新增的位,能夠想象,在原容量下的entry,大部分都不會rehash到新桶裏,只有[]指示的位是1的狀況纔會rehash到新桶裏面,因此rehash操做移動鏈表上一半的元素到新桶裏。另外,原容量下不一樣桶裏面的元素,rehash後也不會出如今相同的桶裏面,其位置仍是取決於非[]指示的位置,跟原容量下的同樣。因此上面這個操做能夠直接連接過去,沒必要擔憂重複被覆蓋的狀況。
// 代碼片斷二 // Clone all remaining nodes for (HashEntry<K,V> p = e; p != lastRun; p = p.next) { int k = p.hash & sizeMask; HashEntry<K,V> n = newTable[k]; newTable[k] = new HashEntry<K,V>(p.key, p.hash, n, p.value); }
size()操做
size操做會先嚐試兩次不加鎖的狀況下計算全部segment的size總數,若是兩次計算的結果相等,說明size是正確的,直接返回這個結果。若是不相等,則全部segment加鎖作一次計算。
推薦閱讀: