Java集合之HashMap

轉載:https://tech.meituan.com/java-hashmap.html

Java為數(shù)據(jù)結構中的映射定義了一個接口java.util.Map守伸,此接口主要有四個常用的實現(xiàn)類施绎,分別是HashMap、Hashtable澎灸、LinkedHashMap和TreeMap,類繼承關系如下圖所示:

java.util.map類圖.png

簡介

HashMap:

它根據(jù)鍵的hashCode值存儲數(shù)據(jù)爽撒,大多數(shù)情況下可以直接定位到它的值蕊唐,因而具有很快的訪問速度,但遍歷順序卻是不確定的简十。

HashMap最多只允許一條記錄的鍵為null檬某,允許多條記錄的值為null。

HashMap非線程安全螟蝙,即任一時刻可以有多個線程同時寫HashMap恢恼,可能會導致數(shù)據(jù)的不一致。如果需要滿足線程安全胰默,可以用 Collections的synchronizedMap方法使HashMap具有線程安全的能力场斑,或者使用ConcurrentHashMap漓踢。

Hashtable:

Hashtable是遺留類,很多映射的常用功能與HashMap類似漏隐,不同的是它承自Dictionary類喧半,并且是線程安全的,任一時間只有一個線程能寫Hashtable青责,并發(fā)性不如ConcurrentHashMap挺据,因為ConcurrentHashMap引入了分段鎖。Hashtable不建議在新代碼中使用脖隶,不需要線程安全的場合可以用HashMap替換扁耐,需要線程安全的場合可以用ConcurrentHashMap替換

LinkedHashMap:

LinkedHashMap是HashMap的一個子類产阱,保存了記錄的插入順序做葵,在用Iterator遍歷LinkedHashMap時,先得到的記錄肯定是先插入的心墅,也可以在構造時帶參數(shù)酿矢,按照訪問次序排序。

LinkedHashMap和HashMap一樣也是非線程安全的怎燥。

TreeMap:

TreeMap實現(xiàn)SortedMap接口瘫筐,能夠把它保存的記錄根據(jù)鍵排序,默認是按鍵值的升序排序铐姚,也可以指定排序的比較器策肝,當用Iterator遍歷TreeMap時,得到的記錄是排過序的隐绵。如果使用排序的映射之众,建議使用TreeMap。在使用TreeMap時依许,key必須實現(xiàn)Comparable接口或者在構造TreeMap傳入自定義的Comparator棺禾,否則會在運行時拋出java.lang.ClassCastException類型的異常。

TreeMap非線程安全峭跳。

對于上述四種Map類型的類膘婶,要求映射中的key是不可變對象。不可變對象是該對象在創(chuàng)建后它的哈希值不會被改變蛀醉。如果對象的哈希值發(fā)生變化悬襟,Map對象很可能就定位不到映射的位置了。

通過上面的比較拯刁,我們知道了HashMap是Java的Map家族中一個普通成員脊岳,鑒于它可以滿足大多數(shù)場景的使用條件,所以是使用頻度最高的一個。下文我們主要結合源碼割捅,從存儲結構奶躯、常用方法分析、擴容以及安全性等方面深入講解HashMap的工作原理棺牧。

HashMap的內部實現(xiàn)

從結構實現(xiàn)來講巫糙,HashMap是數(shù)組+鏈表+紅黑樹(JDK1.8增加了紅黑樹部分)實現(xiàn)的朗儒,如下如所示:

hashMap內存結構圖.png
  • HashMap內部維護著一個非常重要的字段——table數(shù)組:
/**
 * The table, initialized on first use, and resized as
 * necessary. When allocated, length is always a power of two.
 * (We also tolerate length zero in some operations to allow
 * bootstrapping mechanics that are currently not needed.)
 */
transient Node<K,V>[] table;

明顯它是一個Node的數(shù)組颊乘。

我們來看Node[JDK1.8]是何物。

/**
 * Basic hash bin node, used for most entries.  (See below for
 * TreeNode subclass, and in LinkedHashMap for its Entry subclass.)
 */
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;
    }
}

Node是HashMap的一個內部類醉锄,實現(xiàn)了Map.Entry接口乏悄,本質是就是一個映射(鍵值對)。上圖中的每個黑色圓點就是一個Node對象恳不。

  • HashMap就是使用哈希表來存儲的

哈希表為解決沖突檩小,可以采用開放地址法和鏈地址法等來解決問題煤率,Java中HashMap采用了鏈地址法壳嚎。鏈地址法,簡單來說佑女,就是數(shù)組加鏈表的結合卵惦。在每個數(shù)組元素上都一個鏈表結構阻肿,當數(shù)據(jù)被Hash后,得到數(shù)組下標沮尿,把數(shù)據(jù)放在對應下標元素的鏈表上丛塌。例如程序執(zhí)行下面代碼:

map.put("美團","小美");

系統(tǒng)將調用"美團"這個key的hashCode()方法得到其hashCode 值(該方法適用于每個Java對象),然后再通過Hash算法的后兩步運算(高位運算和取模運算畜疾,下文有介紹)來定位該鍵值對的存儲位置赴邻,有時兩個key會定位到相同的位置,表示發(fā)生了Hash碰撞啡捶。當然Hash算法計算結果越分散均勻姥敛,Hash碰撞的概率就越小,map的存取效率就會越高瞎暑。

如果哈希桶數(shù)組很大徒溪,即使較差的Hash算法也會比較分散,如果哈希桶數(shù)組數(shù)組很小金顿,即使好的Hash算法也會出現(xiàn)較多碰撞臊泌,所以就需要在空間成本和時間成本之間權衡,其實就是在根據(jù)實際情況確定哈希桶數(shù)組的大小揍拆,并在此基礎上設計好的hash算法減少Hash碰撞渠概。那么通過什么方式來控制map使得Hash碰撞的概率又小,哈希桶數(shù)組(Node[] table)占用空間又少呢?答案就是好的Hash算法和擴容機制播揪。

在理解Hash和擴容流程之前贮喧,我們得先了解下HashMap的幾個字段。從HashMap的默認構造函數(shù)源碼可知猪狈,構造函數(shù)就是對下面幾個字段進行初始化箱沦,源碼如下:

/**
     * The number of key-value mappings contained in this map.
     */
    transient int size;

    /**
     * The number of times this HashMap has been structurally modified
     * Structural modifications are those that change the number of mappings in
     * the HashMap or otherwise modify its internal structure (e.g.,
     * rehash).  This field is used to make iterators on Collection-views of
     * the HashMap fail-fast.  (See ConcurrentModificationException).
     */
    transient int modCount;

    /**
     * The next size value at which to resize (capacity * load factor).
     *
     * @serial
     */
    // (The javadoc description is true upon serialization.
    // Additionally, if the table array has not been allocated, this
    // field holds the initial array capacity, or zero signifying
    // DEFAULT_INITIAL_CAPACITY.)
    int threshold;  //capacity * load factor(容量×負載因子),當table數(shù)組被占用的個數(shù)超過該值是時雇庙,將調用resize函數(shù)

    /**
     * The load factor for the hash table.
     *
     * @serial
     */
    final float loadFactor; //默認為0.75

首先谓形,Node[] table的初始化長度length(默認值是16),Load factor為負載因子(默認值是0.75)疆前,threshold是HashMap所能容納的最大數(shù)據(jù)量的Node(鍵值對)個數(shù)寒跳。threshold = length * Load factor。也就是說竹椒,在數(shù)組定義好長度之后童太,負載因子越大,所能容納的鍵值對個數(shù)越多胸完。

結合負載因子的定義公式可知书释,threshold就是在此Load factor和length(數(shù)組長度)對應下允許的最大元素數(shù)目,超過這個數(shù)目就重新resize(擴容)赊窥,擴容后的HashMap容量是之前容量的兩倍爆惧。默認的負載因子0.75是對空間和時間效率的一個平衡選擇,建議大家不要修改誓琼,除非在時間和空間比較特殊的情況下检激,如果內存空間很多而又對時間效率要求很高,可以降低負載因子Load factor的值腹侣;相反叔收,如果內存空間緊張而對時間效率要求不高,可以增加負載因子loadFactor的值傲隶,這個值可以大于1饺律。

size這個字段其實很好理解,就是HashMap中實際存在的鍵值對數(shù)量跺株。注意和table的長度length复濒、容納最大鍵值對數(shù)量threshold的區(qū)別。而modCount字段主要用來記錄HashMap內部結構發(fā)生變化的次數(shù)乒省,主要用于迭代的快速失敗巧颈。強調一點,內部結構發(fā)生變化指的是結構發(fā)生變化袖扛,例如put新鍵值對砸泛,但是某個key對應的value值被覆蓋不屬于結構變化十籍。

在HashMap中,哈希桶數(shù)組table的長度length大小必須為2的n次方(一定是合數(shù))唇礁,這是一種非常規(guī)的設計勾栗,常規(guī)的設計是把桶的大小設計為素數(shù)。相對來說素數(shù)導致沖突的概率要小于合數(shù)盏筐,具體證明可以參考http://blog.csdn.net/liuqiyao_01/article/details/14475159围俘,
Hashtable初始化桶大小為11,就是桶大小設計為素數(shù)的應用(Hashtable擴容后不能保證還是素數(shù))琢融。HashMap采用這種非常規(guī)設計界牡,主要是為了在取模和擴容時做優(yōu)化,同時為了減少沖突吏奸,HashMap定位哈希桶索引位置時欢揖,也加入了高位參與運算的過程陶耍。

這里存在一個問題奋蔚,即使負載因子和Hash算法設計的再合理,也免不了會出現(xiàn)拉鏈過長的情況烈钞,一旦出現(xiàn)拉鏈過長泊碑,則會嚴重影響HashMap的性能。于是毯欣,在JDK1.8版本中馒过,對數(shù)據(jù)結構做了進一步的優(yōu)化,引入了紅黑樹酗钞。而當鏈表長度太長(默認超過8)時腹忽,鏈表就轉換為紅黑樹,利用紅黑樹快速增刪改查的特點提高HashMap的性能砚作,其中會用到紅黑樹的插入窘奏、刪除、查找等算法葫录。本文不再對紅黑樹展開討論着裹,想了解更多紅黑樹數(shù)據(jù)結構的工作原理可以參考http://blog.csdn.net/v_july_v/article/details/6105630

功能實現(xiàn)-方法

HashMap的內部功能實現(xiàn)很多米同,本文主要從根據(jù)key獲取哈希桶數(shù)組索引位置骇扇、put方法的詳細執(zhí)行、擴容過程三個具有代表性的點深入展開講解面粮。

確定哈希桶數(shù)組索引位置

不管增加少孝、刪除、查找鍵值對熬苍,定位到哈希桶數(shù)組的位置都是很關鍵的第一步稍走。前面說過HashMap的數(shù)據(jù)結構是數(shù)組和鏈表的結合,所以我們當然希望這個HashMap里面的元素位置盡量分布均勻些,盡量使得每個位置上的元素數(shù)量只有一個钱磅,那么當我們用hash算法求得這個位置的時候梦裂,馬上就可以知道對應位置的元素就是我們要的,不用遍歷鏈表盖淡,大大優(yōu)化了查詢的效率年柠。HashMap定位數(shù)組索引位置,直接決定了hash方法的離散性能褪迟。先看看源碼的實現(xiàn)(方法一+方法二):

方法一:
static final int hash(Object key) {   //jdk1.8 & jdk1.7
     int h;
     // h = key.hashCode() 為第一步 取hashCode值
     // h ^ (h >>> 16)  為第二步 高位參與運算
     return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
方法二:
static int indexFor(int h, int length) {  //jdk1.7的源碼冗恨,jdk1.8沒有這個方法,但是實現(xiàn)原理一樣的
     return h & (length-1);  //第三步 取模運算
}

這里的Hash算法本質上就是三步:取key的hashCode值味赃、高位運算掀抹、取模運算。

對于任意給定的對象心俗,只要它的hashCode()返回值相同傲武,那么程序調用方法一所計算得到的Hash碼值總是相同的。

我們首先想到的就是把hash值對數(shù)組長度取模運算城榛,這樣一來揪利,元素的分布相對來說是比較均勻的。但是狠持,模運算的消耗還是比較大的疟位,在HashMap中是這樣做的:調用方法二來計算該對象應該保存在table數(shù)組的哪個索引處。

這個方法非常巧妙喘垂,它通過h & (table.length -1)來得到該對象的保存位甜刻,而HashMap底層數(shù)組的長度總是2的n次方,這是HashMap在速度上的優(yōu)化正勒。當length總是2的n次方時得院,h& (length-1)運算等價于對length取模,也就是h%length昭齐,但是 & 比 % 具有更高的效率尿招。

在JDK1.8的實現(xiàn)中,優(yōu)化了高位運算的算法阱驾,通過hashCode()的高16位異或低16位實現(xiàn)的:(h = k.hashCode()) ^ (h >>> 16)就谜,主要是從速度、功效里覆、質量來考慮的丧荐,這么做可以在數(shù)組table的length比較小的時候,也能保證考慮到高低Bit都參與到Hash的計算中喧枷,同時不會有太大的開銷虹统。

下面舉例說明下弓坞,n為table的長度:

hashMap哈希算法例圖.png

分析HashMap的put方法

HashMap的put方法執(zhí)行過程可以通過下圖來理解,自己有興趣可以去對比源碼更清楚地研究學習车荔。

hashMap put方法執(zhí)行流程圖.png

①.判斷鍵值對數(shù)組table[i]是否為空或為null渡冻,否則執(zhí)行resize()進行擴容;

②.根據(jù)鍵值key計算hash值得到插入的數(shù)組索引i忧便,如果table[i]==null族吻,直接新建節(jié)點添加,轉向⑥珠增,如果table[i]不為空超歌,轉向③;

③.判斷table[i]的首個元素是否和key一樣蒂教,如果相同直接覆蓋value巍举,否則轉向④,這里的相同指的是hashCode以及equals凝垛;

④.判斷table[i] 是否為treeNode懊悯,即table[i] 是否是紅黑樹,如果是紅黑樹苔严,則直接在樹中插入鍵值對定枷,否則轉向⑤孤澎;

⑤.遍歷table[i]届氢,判斷鏈表長度是否大于8,大于8的話把鏈表轉換為紅黑樹覆旭,在紅黑樹中執(zhí)行插入操作退子,否則進行鏈表的插入操作;遍歷過程中若發(fā)現(xiàn)key已經存在直接覆蓋value即可型将;

⑥.插入成功后寂祥,判斷實際存在的鍵值對數(shù)量size是否超多了最大容量threshold,如果超過七兜,進行擴容丸凭。

map對象調用put成員函數(shù),put函數(shù)的實現(xiàn)如下:

/**
 * Associates the specified value with the specified key in this map.
 * If the map previously contained a mapping for the key, the old
 * value is replaced.
 *
 * @param key key with which the specified value is to be associated
 * @param value value to be associated with the specified key
 * @return the previous value associated with <tt>key</tt>, or
 *         <tt>null</tt> if there was no mapping for <tt>key</tt>.
 *         (A <tt>null</tt> return can also indicate that the map
 *         previously associated <tt>null</tt> with <tt>key</tt>.)
 */
public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}

根據(jù)源碼得知:put函數(shù)調用了putVal函數(shù)腕铸,并返回了putVal函數(shù)的返回值惜犀,而putVal函數(shù)的具體實現(xiàn)如下:

/**
 * Implements Map.put and related methods
 *
 * @param hash hash for key
 * @param key the key
 * @param value the value to put
 * @param onlyIfAbsent if true, don't change existing value
 * @param evict if false, the table is in creation mode.
 * @return previous value, or null if none
 */
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
            boolean evict) {
  Node<K,V>[] tab; Node<K,V> p; int n, i;
  // 步驟①:tab為空則創(chuàng)建
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    // 步驟②:計算index,并對null做處理
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    else {
        Node<K,V> e; K k;
        // 步驟③:節(jié)點key存在狠裹,直接覆蓋value
        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) {
                if ((e = p.next) == null) {
                    p.next = newNode(hash, key,value,null);
                  //鏈表長度大于8轉換為紅黑樹進行處理
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);
                    break;
                }
              // key已經存在直接覆蓋value
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                          break;
                p = e;
            }
        }

        if (e != null) { // existing mapping for key
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }
    }

    ++modCount;
    // 步驟⑥:超過最大容量 就擴容
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
}

擴容機制

擴容(resize)就是重新計算容量虽界,向HashMap對象里不停的添加元素,而HashMap對象內部的數(shù)組無法裝載更多的元素時涛菠,對象就需要擴大數(shù)組的長度莉御,以便能裝入更多的元素撇吞。當然Java里的數(shù)組是無法自動擴容的,方法是使用一個新的數(shù)組代替已有的容量小的數(shù)組礁叔,就像我們用一個小桶裝水牍颈,如果想裝更多的水,就得換大水桶琅关。

我們分析下resize的源碼颂砸,鑒于JDK1.8融入了紅黑樹,較復雜死姚,為了便于理解我們仍然使用JDK1.7的代碼人乓,好理解一些,本質上區(qū)別不大都毒,具體區(qū)別后文再說色罚。

JDK1.7中HashMap的resize()源碼:

void resize(int newCapacity) {   //傳入新的容量
  Entry[] oldTable = table;    //引用擴容前的Entry數(shù)組
  int oldCapacity = oldTable.length;
  if (oldCapacity == MAXIMUM_CAPACITY) {  //擴容前的數(shù)組大小如果已經達到最大(2^30)了
      threshold = Integer.MAX_VALUE; //修改閾值為int的最大值(2^31-1),這樣以后就不會擴容了
      return;
  }

  Entry[] newTable = new Entry[newCapacity];  //初始化一個新的Entry數(shù)組
  transfer(newTable);                         //U司ⅰ戳护!將數(shù)據(jù)轉移到新的Entry數(shù)組里
  table = newTable;                           //HashMap的table屬性引用新的Entry數(shù)組
  threshold = (int)(newCapacity * loadFactor);//修改閾值
}

這里就是使用一個容量更大的數(shù)組來代替已有的容量小的數(shù)組,transfer()方法將原有Entry數(shù)組的元素拷貝到新的Entry數(shù)組里瀑焦。

void transfer(Entry[] newTable) {
    Entry[] src = table;                   //src引用了舊的Entry數(shù)組
    int newCapacity = newTable.length;
    for (int j = 0; j < src.length; j++) { //遍歷舊的Entry數(shù)組
        Entry<K,V> e = src[j];             //取得舊Entry數(shù)組的每個元素
        if (e != null) {
            src[j] = null;//釋放舊Entry數(shù)組的對象引用(for循環(huán)后腌且,舊的Entry數(shù)組不再引用任何對象)
            do {
               Entry<K,V> next = e.next;
               int i = indexFor(e.hash, newCapacity); //!榛瓮!重新計算每個元素在數(shù)組中的位置
               e.next = newTable[i]; //標記[1]
               newTable[i] = e;      //將元素放在數(shù)組上
               e = next;             //訪問下一個Entry鏈上的元素
           } while (e != null);
       }
   }
 }

newTable[i]的引用賦給了e.next铺董,也就是使用了單鏈表的頭插入方式,同一位置上新元素總會被放在鏈表的頭部位置禀晓;這樣先放在一個索引上的元素終會被放到Entry鏈的尾部(如果發(fā)生了hash沖突的話)精续,這一點和Jdk1.8有區(qū)別,下文詳解粹懒。在舊數(shù)組中同一條Entry鏈上的元素重付,通過重新計算索引位置后,有可能被放到了新數(shù)組的不同位置上凫乖。

下面舉個例子說明下擴容過程确垫。假設了我們的hash算法就是簡單的用key mod 一下表的大小(也就是數(shù)組的長度)帽芽。其中的哈希桶數(shù)組table的size=2删掀, 所以key = 3、7嚣镜、5爬迟,put順序依次為 5、7菊匿、3付呕。在mod 2以后都沖突在table[1]這里了计福。這里假設負載因子 loadFactor=1,即當鍵值對的實際大小size 大于 table的實際大小時進行擴容徽职。接下來的三個步驟是哈希桶數(shù)組 resize成4象颖,然后所有的Node重新rehash的過程。

jdk1.7擴容例圖.png

下面我們講解下JDK1.8做了哪些優(yōu)化姆钉。經過觀測可以發(fā)現(xiàn)说订,我們使用的是2次冪的擴展(指長度擴為原來2倍),所以潮瓶,元素的位置要么是在原位置陶冷,要么是在原位置再移動2次冪的位置√焊ǎ看下圖可以明白這句話的意思埂伦,n為table的長度,圖(a)表示擴容前的key1和key2兩種key確定索引位置的示例思恐,圖(b)表示擴容后key1和key2兩種key確定索引位置的示例沾谜,其中hash1是key1對應的哈希與高位運算結果。

hashMap 1.8 哈希算法例圖1.png

元素在重新計算hash之后胀莹,因為n變?yōu)?倍基跑,那么n-1的mask范圍在高位多1bit(紅色),因此新的index就會發(fā)生這樣的變化:

hashMap 1.8 哈希算法例圖2.png

因此描焰,我們在擴充HashMap的時候媳否,不需要像JDK1.7的實現(xiàn)那樣重新計算hash,只需要看看原來的hash值新增的那個bit是1還是0就好了栈顷,是0的話索引沒變逆日,是1的話索引變成“原索引+oldCap”,可以看看下圖為16擴充為32的resize示意圖:

jdk1.8 hashMap擴容例圖.png

這個設計確實非常的巧妙萄凤,既省去了重新計算hash值的時間,而且同時搪哪,由于新增的1bit是0還是1可以認為是隨機的靡努,因此resize的過程,均勻的把之前的沖突的節(jié)點分散到新的bucket了晓折。這一塊就是JDK1.8新增的優(yōu)化點惑朦。有一點注意區(qū)別,JDK1.7中rehash的時候漓概,舊鏈表遷移新鏈表的時候漾月,如果在新表的數(shù)組索引位置相同,則鏈表元素會倒置胃珍,但是從上圖可以看出梁肿,JDK1.8不會倒置蜓陌。有興趣的同學可以研究下JDK1.8的resize源碼,寫的很贊吩蔑,如下:

/**
    * Initializes or doubles table size.  If null, allocates in
    * accord with initial capacity target held in field threshold.
    * Otherwise, because we are using power-of-two expansion, the
    * elements from each bin must either stay at same index, or move
    * with a power of two offset in the new table.
    *
    * @return the table
    */
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;
        }
        // 沒超過最大值,就擴充為原來的2倍
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                 oldCap >= DEFAULT_INITIAL_CAPACITY)
            newThr = oldThr << 1; // double threshold
    }
    else if (oldThr > 0) // initial capacity was placed in threshold
        newCap = oldThr;
    else {               // zero initial threshold signifies using defaults
        newCap = DEFAULT_INITIAL_CAPACITY;
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    // 計算新的resize上限
    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) {
        // 把每個bucket都移動到新的buckets中
        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 { // 鏈表優(yōu)化重hash的代碼塊
                    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;
                        }
                        // 原索引+oldCap
                        else {
                            if (hiTail == null)
                                hiHead = e;
                            else
                                hiTail.next = e;
                            hiTail = e;
                        }
                    } while ((e = next) != null);
                    // 原索引放到bucket里
                    if (loTail != null) {
                        loTail.next = null;
                        newTab[j] = loHead;
                    }
                    // 原索引+oldCap放到bucket里
                    if (hiTail != null) {
                        hiTail.next = null;
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    return newTab;
}

線程安全性

在多線程使用場景中隧期,應該盡量避免使用線程不安全的HashMap,而使用線程安全的ConcurrentHashMap赘娄。那么為什么說HashMap是線程不安全的仆潮,下面舉例子說明在并發(fā)的多線程使用場景中使用HashMap可能造成死循環(huán)。代碼例子如下(便于理解遣臼,仍然使用JDK1.7的環(huán)境):

public class HashMapInfiniteLoop {

    private static HashMap<Integer,String> map = new HashMap<Integer,String>(2鸵闪,0.75f);
    public static void main(String[] args) {
        map.put(5, "C");

        new Thread("Thread1") {
            public void run() {
                map.put(7, "B");
                System.out.println(map);
            };
        }.start();
        new Thread("Thread2") {
            public void run() {
                map.put(3, "A");
                System.out.println(map);
            };
        }.start();
    }
}

其中暑诸,map初始化為一個長度為2的數(shù)組蚌讼,loadFactor=0.75,threshold=2*0.75=1个榕,也就是說當put第二個key的時候篡石,map就需要進行resize。

通過設置斷點讓線程1和線程2同時debug到transfer方法(3.3小節(jié)代碼塊)的首行西采。注意此時兩個線程已經成功添加數(shù)據(jù)凰萨。放開thread1的斷點至transfer方法的“Entry next = e.next;” 這一行;然后放開線程2的的斷點械馆,讓線程2進行resize胖眷。結果如下圖。

jdk1.7 hashMap死循環(huán)例圖1.png

注意霹崎,Thread1的 e 指向了key(3)珊搀,而next指向了key(7),其在線程二rehash后尾菇,指向了線程二重組后的鏈表境析。

線程一被調度回來執(zhí)行,先是執(zhí)行 newTalbe[i] = e派诬, 然后是e = next劳淆,導致了e指向了key(7),而下一次循環(huán)的next = e.next導致了next指向了key(3)默赂。

jdk1.7 hashMap死循環(huán)例圖2.png
jdk1.7 hashMap死循環(huán)例圖3.png

e.next = newTable[i] 導致 key(3).next 指向了 key(7)沛鸵。注意:此時的key(7).next 已經指向了key(3), 環(huán)形鏈表就這樣出現(xiàn)了缆八。

jdk1.7 hashMap死循環(huán)例圖4.png

于是曲掰,當我們用線程一調用map.get(11)時疾捍,悲劇就出現(xiàn)了——Infinite Loop。

JDK1.8與JDK1.7的性能對比

HashMap中蜈缤,如果key經過hash算法得出的數(shù)組索引位置全部不相同拾氓,即Hash算法非常好,那樣的話底哥,getKey方法的時間復雜度就是O(1)咙鞍,如果Hash算法技術的結果碰撞非常多,假如Hash算極其差趾徽,所有的Hash算法結果得出的索引位置一樣续滋,那樣所有的鍵值對都集中到一個桶中,或者在一個鏈表中孵奶,或者在一個紅黑樹中疲酌,時間復雜度分別為O(n)和O(lgn)。 鑒于JDK1.8做了多方面的優(yōu)化了袁,總體性能優(yōu)于JDK1.7朗恳,下面我們從兩個方面用例子證明這一點。

Hash較均勻的情況

為了便于測試载绿,我們先寫一個類Key粥诫,如下:

class Key implements Comparable<Key> {

    private final int value;

    Key(int value) {
        this.value = value;
    }

    @Override
    public int compareTo(Key o) {
        return Integer.compare(this.value, o.value);
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass())
            return false;
        Key key = (Key) o;
        return value == key.value;
    }

    @Override
    public int hashCode() {
        return value;
    }
}

這個類復寫了equals方法,并且提供了相當好的hashCode函數(shù)怀浆,任何一個值的hashCode都不會相同,因為直接使用value當做hashcode怕享。為了避免頻繁的GC执赡,我將不變的Key實例緩存了起來,而不是一遍一遍的創(chuàng)建它們函筋。代碼如下:

public class Keys {

    public static final int MAX_KEY = 10_000_000;
    private static final Key[] KEYS_CACHE = new Key[MAX_KEY];

    static {
        for (int i = 0; i < MAX_KEY; ++i) {
            KEYS_CACHE[i] = new Key(i);
        }
    }

    public static Key of(int value) {
        return KEYS_CACHE[value];
    }
}

現(xiàn)在開始我們的試驗沙合,測試需要做的僅僅是,創(chuàng)建不同size的HashMap(1驻呐、10灌诅、100、……10000000)含末,屏蔽了擴容的情況,代碼如下:

static void test(int mapSize) {

   HashMap<Key, Integer> map = new HashMap<Key,Integer>(mapSize);
   for (int i = 0; i < mapSize; ++i) {
       map.put(Keys.of(i), i);
   }

   long beginTime = System.nanoTime(); //獲取納秒
   for (int i = 0; i < mapSize; i++) {
       map.get(Keys.of(i));
   }
   long endTime = System.nanoTime();
   System.out.println(endTime - beginTime);
}

public static void main(String[] args) {
   for(int i=10;i<= 1000 0000;i*= 10){
       test(i);
   }
}

在測試中會查找不同的值即舌,然后度量花費的時間佣盒,為了計算getKey的平均時間,我們遍歷所有的get方法顽聂,計算總的時間肥惭,除以key的數(shù)量盯仪,計算一個平均值,主要用來比較蜜葱,絕對值可能會受很多環(huán)境因素的影響全景。結果如下:

性能比較表1.png

通過觀測測試結果可知,JDK1.8的性能要高于JDK1.7 15%以上牵囤,在某些size的區(qū)域上爸黄,甚至高于100%。由于Hash算法較均勻揭鳞,JDK1.8引入的紅黑樹效果不明顯炕贵,下面我們看看Hash不均勻的的情況。

Hash極不均勻的情況

假設我們又一個非常差的Key野崇,它們所有的實例都返回相同的hashCode值称开。這是使用HashMap最壞的情況。代碼修改如下:

class Key implements Comparable<Key> {
 
    //...
 
    @Override
    public int hashCode() {
        return 1;
    }
}

仍然執(zhí)行main方法乓梨,得出的結果如下表所示:

性能比較表2.png

從表中結果中可知鳖轰,隨著size的變大,JDK1.7的花費時間是增長的趨勢扶镀,而JDK1.8是明顯的降低趨勢蕴侣,并且呈現(xiàn)對數(shù)增長穩(wěn)定。當一個鏈表太長的時候狈惫,HashMap會動態(tài)的將它替換成一個紅黑樹睛蛛,這話的話會將時間復雜度從O(n)降為O(logn)。hash算法均勻和不均勻所花費的時間明顯也不相同胧谈,這兩種情況的相對比較忆肾,可以說明一個好的hash算法的重要性。

測試環(huán)境:處理器為2.2 GHz Intel Core i7菱肖,內存為16 GB 1600 MHz DDR3客冈,SSD硬盤,使用默認的JVM參數(shù)稳强,運行在64位的OS X 10.10.1上场仲。

小結

(1) 擴容是一個特別耗性能的操作,所以當程序員在使用HashMap的時候退疫,估算map的大小渠缕,初始化的時候給一個大致的數(shù)值,避免map進行頻繁的擴容褒繁。

(2) 負載因子是可以修改的亦鳞,也可以大于1,但是建議不要輕易修改,除非情況非常特殊燕差。

(3) HashMap是線程不安全的遭笋,不要在并發(fā)的環(huán)境中同時操作HashMap,建議使用ConcurrentHashMap徒探。

(4) JDK1.8引入紅黑樹大程度優(yōu)化了HashMap的性能瓦呼。

(5) 還沒升級JDK1.8的,現(xiàn)在開始升級吧测暗。HashMap的性能提升僅僅是JDK1.8的冰山一角央串。

參考

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市俭厚,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌饮戳,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,919評論 6 502
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件洞拨,死亡現(xiàn)場離奇詭異扯罐,居然都是意外死亡,警方通過查閱死者的電腦和手機烦衣,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,567評論 3 392
  • 文/潘曉璐 我一進店門篮赢,熙熙樓的掌柜王于貴愁眉苦臉地迎上來齿椅,“玉大人琉挖,你說我怎么就攤上這事启泣。” “怎么了示辈?”我有些...
    開封第一講書人閱讀 163,316評論 0 353
  • 文/不壞的土叔 我叫張陵寥茫,是天一觀的道長。 經常有香客問我矾麻,道長纱耻,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,294評論 1 292
  • 正文 為了忘掉前任险耀,我火速辦了婚禮弄喘,結果婚禮上,老公的妹妹穿的比我還像新娘甩牺。我一直安慰自己蘑志,他們只是感情好,可當我...
    茶點故事閱讀 67,318評論 6 390
  • 文/花漫 我一把揭開白布贬派。 她就那樣靜靜地躺著急但,像睡著了一般。 火紅的嫁衣襯著肌膚如雪搞乏。 梳的紋絲不亂的頭發(fā)上波桩,一...
    開封第一講書人閱讀 51,245評論 1 299
  • 那天,我揣著相機與錄音请敦,去河邊找鬼镐躲。 笑死,一個胖子當著我的面吹牛侍筛,可吹牛的內容都是我干的萤皂。 我是一名探鬼主播,決...
    沈念sama閱讀 40,120評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼勾笆,長吁一口氣:“原來是場噩夢啊……” “哼敌蚜!你這毒婦竟也來了?” 一聲冷哼從身側響起窝爪,我...
    開封第一講書人閱讀 38,964評論 0 275
  • 序言:老撾萬榮一對情侶失蹤弛车,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后蒲每,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體纷跛,經...
    沈念sama閱讀 45,376評論 1 313
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,592評論 2 333
  • 正文 我和宋清朗相戀三年邀杏,在試婚紗的時候發(fā)現(xiàn)自己被綠了贫奠。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片唬血。...
    茶點故事閱讀 39,764評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖唤崭,靈堂內的尸體忽然破棺而出拷恨,到底是詐尸還是另有隱情,我是刑警寧澤谢肾,帶...
    沈念sama閱讀 35,460評論 5 344
  • 正文 年R本政府宣布腕侄,位于F島的核電站,受9級特大地震影響芦疏,放射性物質發(fā)生泄漏冕杠。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,070評論 3 327
  • 文/蒙蒙 一酸茴、第九天 我趴在偏房一處隱蔽的房頂上張望分预。 院中可真熱鬧,春花似錦薪捍、人聲如沸笼痹。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,697評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽与倡。三九已至,卻和暖如春昆稿,著一層夾襖步出監(jiān)牢的瞬間纺座,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,846評論 1 269
  • 我被黑心中介騙來泰國打工溉潭, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留净响,地道東北人。 一個月前我還...
    沈念sama閱讀 47,819評論 2 370
  • 正文 我出身青樓喳瓣,卻偏偏與公主長得像馋贤,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子畏陕,可洞房花燭夜當晚...
    茶點故事閱讀 44,665評論 2 354

推薦閱讀更多精彩內容

  • public class HashMap<K,V> extends AbstractMap<K,V>impleme...
    yuruihua閱讀 276評論 1 0
  • 摘要 HashMap是Java程序員使用頻率最高的用于映射(鍵值對)處理的數(shù)據(jù)類型配乓。隨著JDK(Java Deve...
    周二倩你一生閱讀 1,250評論 0 5
  • 前言 HashMap是Map集合的一種實現(xiàn),提供了一種簡單實用的數(shù)據(jù)存儲和讀取方式惠毁。Map接口不同于List接口犹芹,...
    帶娃兒先走閱讀 199評論 0 0
  • 有一天晚上看到群里一個女生問大家一個問題: 你們有沒有因為彩禮和男朋友鬧得不愉快的把 ? 好幾個女生說蜈膨,有屿笼,因為當?shù)?..
    污妖水瓶王閱讀 645評論 0 1
  • 先來輕松一下驴一,講講昨夜臨睡之前休雌,我看的一部家庭奇幻劇--一個人,也是一只貓蛔趴。 公司老總 一位當?shù)赜忻钠髽I(yè)家挑辆,坐擁...
    蘼蝶音閱讀 273評論 0 0