Java基礎——經常使用Map的實現細節

Java基礎——Map

HashMap

  1. 數據結構: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;
  2. 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);
     }
  3. 擴容:併發

    當數據量達到閥值(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);
         	}
     	}
     }
  4. fail-fast:性能

    HashMap是非線程安全的集合,在進行HashMap遍歷(HashIterator)操做時,若是map有修改操做,都會增長modCount的值,經過modCount與expectModCount進行比較,若是二者不相等,當即拋出ConcurrentModificationException。

LinkedHashMap

  1. 數據結構:

    數組 + 單鏈表 + 雙向鏈表

    LinkedHashMap是在HashMap的基礎上添加了一個雙向鏈表結構,來按必定的順序維護裏面的全部entry。

    Entry<K,V> before, after;//雙向鏈表結構
     header = new Entry<K,V>(-1, null, null, null);
     header.before = header.after = header;
  2. 順序性:

    提供兩種順序方式來維護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;
     }
  3. LRU

    LinkedHashMap自然支持LRU操做,即Access-Ordered,默認不開啓。

    protected boolean removeEldestEntry(Map.Entry eldest) {
     	return false;// 默認是返回false,咱們能夠實現這個方法來支持LRU
     }
  4. 其餘

    非線程安全,fast-fail(同hashmap)

ConcurrentHashMap

  1. ConcurrentHashMap採用了鎖分離的技術來實現確保線程安全的狀況下達到較好的性能。它把整個hash table分紅好多個小的hash table(即Segment),每一個Segment都有本身的鎖來保證線程安全,這樣就使得各個Segment均可以獨立地進行管理,而不須要爭用鎖。

  2. 兩個重要的結構: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操做時能夠直接加鎖使用。

  3. 擴容(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);
	}
  1. size()操做

    size操做會先嚐試兩次不加鎖的狀況下計算全部segment的size總數,若是兩次計算的結果相等,說明size是正確的,直接返回這個結果。若是不相等,則全部segment加鎖作一次計算。

推薦閱讀:

Java基礎——同步與鎖

MySQL使用與優化總結

相關文章
相關標籤/搜索