HashMap詳解

 

 

HashMap源碼分析


 

構造函數

 

讓咱們先從構造函數提及,HashMap有四個構造方法,別慌php

 

1.1 HashMap()

 

// 1.無參構造方法、
// 構造一個空的HashMap,初始容量爲16,負載因子爲0.75
public HashMap() {
    this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}

 

無參構造方法就沒什麼好說的了。java

 

1.2 HashMap(int initialCapacity)

 

// 2.構造一個初始容量爲initialCapacity,負載因子爲0.75的空的HashMap,
public HashMap(int initialCapacity) {
    this(initialCapacity, DEFAULT_LOAD_FACTOR);
}

 

HashMap(int initialCapacity) 這個構造方法調用了1.3中的構造方法。node

 

1.3 HashMap(int initialCapacity, float loadFactor)

 

// 3.構造一個空的初始容量爲initialCapacity,負載因子爲loadFactor的HashMap
    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;
        this.threshold = tableSizeFor(initialCapacity);
    }

    //最大容量
    //static final int MAXIMUM_CAPACITY = 1 << 30;

 

當指定的初始容量< 0時拋出IllegalArgumentException異常,當指定的初始容量> MAXIMUM_CAPACITY時,就讓初始容量 = MAXIMUM_CAPACITY。算法

 

當負載因子小於0或者不是數字時,拋出IllegalArgumentException異常。bootstrap

 

設定threshold。 這個threshold = capacity * load factor 。當HashMap的size到了threshold時,就要進行resize,也就是擴容。數組

 

tableSizeFor()的主要功能是返回一個比給定整數大且最接近的2的冪次方整數,如給定10,返回2的4次方16.安全

 

咱們進入tableSizeFor(int cap)的源碼中看看:數據結構

 

// Returns a power of two size for the given target capacity.
  static final int tableSizeFor(int cap) {
    int n = cap - 1;
    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;
  }

 

note: HashMap要求容量必須是2的冪。多線程

 

首先,int n = cap -1是爲了防止cap已是2的冪時,執行完後面的幾條無符號右移操做以後,返回的capacity是這個cap的2倍,由於cap已是2的冪了,就已經知足條件了。 若是不懂能夠往下看完幾個無符號移位後再回來看。(建議本身在紙上畫一下)併發

 

若是n這時爲0了(通過了cap-1以後),則通過後面的幾回無符號右移依然是0,最後返回的capacity是1(最後有個n+1的操做)。這裏只討論n不等於0的狀況

 

以16位爲例,假設開始時 n 爲 0000 1xxx xxxx xxxx  (x表明不關心0仍是1)

 

第一次右移 n |= n >>> 1

 

因爲n不等於0,則n的二進制表示中總會有一bit爲1,這時考慮最高位的1。經過無符號右移1位,則將最高位的1右移了1位,再作或操做,使得n的二進制表示中與最高位的1緊鄰的右邊一位也爲1,如0000 11xx xxxx xxxx 。

 

第二次右移 n |= n >>> 2

 

注意,這個n已經通過了n |= n >>> 1; 操做。此時n爲0000 11xx xxxx xxxx ,則n無符號右移兩位,會將最高位兩個連續的1右移兩位,而後再與原來的n作或操做,這樣n的二進制表示的高位中會有4個連續的1。如0000 1111 xxxx xxxx 。

 

第三次右移 n |= n >>> 4

 

此次把已經有的高位中的連續的4個1,右移4位,再作或操做,這樣n的二進制表示的高位中會有8個連續的1。如0000 1111 1111 xxxx 。

 

第。。。,你還忍心讓我繼續推麼?相信聰明的你已經想出來了,容量最大也就是32位的正數,因此最後一次     n |= n >>> 16; 能夠保證最高位後面的所有置爲1。固然若是是32個1的話,此時超出了MAXIMUM_CAPACITY ,因此取值到 MAXIMUM_CAPACITY 。

 

watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=

tableSizeFor示例圖

 

注意,獲得的這個capacity卻被賦值給了threshold。 這裏我和這篇博客的博主開始的想法同樣,認爲應該這麼寫:

 

this.threshold = tableSizeFor(initialCapacity) * this.loadFactor;

 

由於這樣子才符合threshold的定義:

 

threshold = capacity * load factor;

 

可是,請注意,在構造方法中,並無對table這個成員變量進行初始化,table的初始化被推遲到了put方法中,在put方法中會對threshold從新計算 。

 

我說一下我在理解這個tableSizeFor函數中間遇到的坑吧,我在想若是n=-1時的狀況,由於初始容量能夠傳進來0。我將n= -1 和下面幾條運算一塊兒新寫了個測試程序,發現輸出都是 -1。 

 

這是由於計算機中數字是由補碼存儲的,-1的補碼是 0xffffffff。因此無符號右移以後再進行或運算以後仍是 -1。 

 

那我想若是就無符號右移呢? 好比-1>>>10。聽我娓娓道來,32個1無符號右移10位後,高10位爲0,低22位爲1,此時這個數變成了正數,因爲正數的補碼和原碼相同,因此就變成了0x3FFFFF即10進制的4194303。真刺激。

 

好開森,這個構造方法咱們算是拿下了。怎麼樣,我猜你如今必定很激動,Hey,old  Fe,這纔剛開始。接下來看最後一個構造方法。

 

1.4 HashMap(Map<? extends K, ? extends V> m)

 

// 4. 構造一個和指定Map有相同mappings的HashMap,初始容量能充足的容下指定的Map,負載因子爲0.75
  public HashMap(Map<? extends K, ? extends V> m) {
        this.loadFactor = DEFAULT_LOAD_FACTOR;
        putMapEntries(m, false);
    }

 

套路,直接看 putMapEntries(m,false) 。源碼以下:

 

/**
   * 將m的全部元素存入本HashMap實例中
   */
  final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
    // 獲得 m 中元素的個數
    int s = m.size();
    // 當 m 中有元素時,則需將map中元素放入本HashMap實例。
    if (s > 0) {
      // 判斷table是否已經初始化,若是未初始化,則先初始化一些變量。(table初始化是在put時)
      if (table == null) { // pre-size
        // 根據待插入的map 的 size 計算要建立的 HashMap 的容量。
        float ft = ((float) s / loadFactor) + 1.0F;
        int t = ((ft < (float) MAXIMUM_CAPACITY) ? (int) ft : MAXIMUM_CAPACITY);
        // 把要建立的 HashMap 的容量存在 threshold 中
        if (t > threshold)
          threshold = tableSizeFor(t);
      }
      // 若是table初始化過,由於別的函數也會調用它,因此有可能HashMap已經被初始化過了。
      // 判斷待插入的 map 的 size,若 size 大於 threshold,則先進行 resize(),進行擴容
      else if (s > threshold)
        resize();
      // 而後就開始遍歷 帶插入的 map ,將每個 <Key ,Value> 插入到本HashMap實例。
      for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
        K key = e.getKey();
        V value = e.getValue();
        // put(K,V)也是調用 putVal 函數進行元素的插入
        putVal(hash(key), key, value, false, evict);
      }
    }
  }

 

介紹putVal方法前,說一下HashMap的幾個重要的成員變量:

 

/**
   * 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.)
   */
  // 實際存儲key,value的數組,只不過key,value被封裝成Node了
  transient Node<K, V>[] table;

  /**
   * 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.)
  // 由於 tableSizeFor(int) 返回值給了threshold
  int threshold;

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

 

其實就是哈希表。HashMap使用鏈表法避免哈希衝突(相同hash值),當鏈表長度大於TREEIFY_THRESHOLD(默認爲8)時,將鏈表轉換爲紅黑樹,固然小於UNTREEIFY_THRESHOLD(默認爲6)時,又會轉回鏈表以達到性能均衡。 咱們看一張HashMap的數據結構(數組+鏈表+紅黑樹 )就更能理解table了:

 

watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=

HashMap的數據結構

 

回到putMapEntries函數中,若是table爲null,那麼這時就設置合適的threshold,若是不爲空而且指定的map的size>threshold,那麼就resize()。而後把指定的map的全部Key,Value,經過putVal添加到咱們建立的新的map中。

 

putVal中傳入了個hash(key),那咱們就先來看看hash(key):

 

/**
   * key 的 hash值的計算是經過hashCode()的高16位異或低16位實現的:(h = k.hashCode()) ^ (h >>> 16)
   * 主要是從速度、功效、質量來考慮的,這麼作能夠在數組table的length比較小的時候
   * 也能保證考慮到高低Bit都參與到Hash的計算中,同時不會有太大的開銷
   */
  static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
  }

 

異或運算:

 

(h = key.hashCode()) ^ (h >>> 16)

 

原 來 的 hashCode : 

1111 1111 1111 1111 0100 1100 0000 1010
移位後的hashCode: 

0000 0000 0000 0000 1111 1111 1111 1111
進行異或運算 結果:

1111 1111 1111 1111 1011 0011 1111 0101

 

這樣作的好處是,能夠將hashcode高位和低位的值進行混合作異或運算,並且混合後,低位的信息中加入了高位的信息,這樣高位的信息被變相的保留了下來。摻雜的元素多了,那麼生成的hash值的隨機性會增大。

 

剛纔咱們漏掉了resize()和putVal() 兩個函數,如今咱們按順序分析一波:

 

首先resize() ,先看一下哪些函數調用了resize(),從而在總體上有個概念:

 

watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=

調用了resize的函數.png

 

接下來上源碼:

 

final Node<K,V>[] resize() {
        // 保存當前table
        Node<K,V>[] oldTab = table;
        // 保存當前table的容量
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        // 保存當前閾值
        int oldThr = threshold;
        // 初始化新的table容量和閾值 
        int newCap, newThr = 0;
        /*
        1. resize()函數在size > threshold時被調用。oldCap大於 0 表明原來的 table 表非空,
           oldCap 爲原表的大小,oldThr(threshold) 爲 oldCap × load_factor
        */
        if (oldCap > 0) {
            // 若舊table容量已超過最大容量,更新閾值爲Integer.MAX_VALUE(最大整形值),這樣之後就不會自動擴容了。
            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
        }
        /*
        2. resize()函數在table爲空被調用。oldCap 小於等於 0 且 oldThr 大於0,表明用戶建立了一個 HashMap,可是使用的構造函數爲      
           HashMap(int initialCapacity, float loadFactor) 或 HashMap(int initialCapacity)
           或 HashMap(Map<? extends K, ? extends V> m),致使 oldTab 爲 null,oldCap 爲0, oldThr 爲用戶指定的 HashMap的初始容量。
      */
        else if (oldThr > 0) // initial capacity was placed in threshold
            //當table沒初始化時,threshold持有初始容量。還記得threshold = tableSizeFor(t)麼;
            newCap = oldThr;
        /*
        3. resize()函數在table爲空被調用。oldCap 小於等於 0 且 oldThr 等於0,用戶調用 HashMap()構造函數建立的 HashMap,全部值均採用默認值,oldTab(Table)表爲空,oldCap爲0,oldThr等於0,
        */
        else {               // zero initial threshold signifies using defaults
            newCap = DEFAULT_INITIAL_CAPACITY;
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
        // 新閾值爲0
        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"})
        // 初始化table
        Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
        table = newTab;
        if (oldTab != null) {
            // 把 oldTab 中的節點 reHash 到 newTab 中去
            for (int j = 0; j < oldCap; ++j) {
                Node<K,V> e;
                if ((e = oldTab[j]) != null) {
                    oldTab[j] = null;
                    // 若節點是單個節點,直接在 newTab 中進行重定位
                    if (e.next == null)
                        newTab[e.hash & (newCap - 1)] = e;
                    // 若節點是 TreeNode 節點,要進行 紅黑樹的 rehash 操做
                    else if (e instanceof TreeNode)
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                    // 如果鏈表,進行鏈表的 rehash 操做
                    else { // preserve order
                        Node<K,V> loHead = null, loTail = null;
                        Node<K,V> hiHead = null, hiTail = null;
                        Node<K,V> next;
                        // 將同一桶中的元素根據(e.hash & oldCap)是否爲0進行分割(代碼後有圖解,能夠回過頭再來看),分紅兩個不一樣的鏈表,完成rehash
                        do {
                            next = e.next;
                            // 根據算法 e.hash & oldCap 判斷節點位置rehash 後是否發生改變
                            //最高位==0,這是索引不變的鏈表。
                            if ((e.hash & oldCap) == 0) { 
                                if (loTail == null)
                                    loHead = e;
                                else
                                    loTail.next = e;
                                loTail = e;
                            }
                            //最高位==1 (這是索引起生改變的鏈表)
                            else {  
                                if (hiTail == null)
                                    hiHead = e;
                                else
                                    hiTail.next = e;
                                hiTail = e;
                            }
                        } while ((e = next) != null);
                        if (loTail != null) {  // 原bucket位置的尾指針不爲空(即還有node)  
                            loTail.next = null; // 鏈表最後得有個null
                            newTab[j] = loHead; // 鏈表頭指針放在新桶的相同下標(j)處
                        }
                        if (hiTail != null) {
                            hiTail.next = null;
                            // rehash 後節點新的位置必定爲原來基礎上加上 oldCap,具體解釋看下圖
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;
    }
}

 

引自美團點評技術博客。咱們使用的是2次冪的擴展(指長度擴爲原來2倍),因此,元素的位置要麼是在原位置,要麼是在原位置再移動2次冪的位置。看下圖能夠明白這句話的意思,n爲table的長度,圖(a)表示擴容前的key1和key2兩種key肯定索引位置的示例,圖(b)表示擴容後key1和key2兩種key肯定索引位置的示例,其中hash1是key1對應的哈希與高位運算結果。

 

watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=

hashMap 1.8 哈希算法例圖1.png

 

元素在從新計算hash以後,由於n變爲2倍,那麼n-1的mask範圍在高位多1bit(紅色),所以新的index就會發生這樣的變化:

 

watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=

hashMap 1.8 哈希算法例圖2.png

 

所以,咱們在擴充HashMap的時候,只須要看看原來的hash值新增的那個bit是1仍是0就行了,是0的話索引沒變,是1的話索引變成「原索引+oldCap」,能夠看看下圖爲16擴充爲32的resize示意圖 :

 

watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=

jdk1.8 hashMap擴容例圖.png

 

何時擴容:經過HashMap源碼能夠看到是在put操做時,即向容器中添加元素時,判斷當前容器中元素的個數是否達到閾值(當前數組長度乘以加載因子的值)的時候,就要自動擴容了。

 

擴容(resize):其實就是從新計算容量;而這個擴容是計算出所需容器的大小以後從新定義一個新的容器,將原來容器中的元素放入其中。

 

resize()告一段落,接下來看 putVal() 。

 

上源碼:

 

//實現put和相關方法。
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        //若是table爲空或者長度爲0,則resize()
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        //肯定插入table的位置,算法是(n - 1) & hash,在n爲2的冪時,至關於取摸操做。
        ////找到key值對應的槽而且是第一個,直接加入
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        //在table的i位置發生碰撞,有兩種狀況,一、key值是同樣的,替換value值,
        //二、key值不同的有兩種處理方式:2.一、存儲在i位置的鏈表;2.二、存儲在紅黑樹中
        else {
            Node<K,V> e; K k;
            //第一個node的hash值即爲要加入元素的hash
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            //2.2
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            //2.1
            else {
                //不是TreeNode,即爲鏈表,遍歷鏈表
                for (int binCount = 0; ; ++binCount) {
                ///鏈表的尾端也沒有找到key值相同的節點,則生成一個新的Node,
                //而且判斷鏈表的節點個數是否是到達轉換成紅黑樹的上界達到,則轉換成紅黑樹。
                    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;
                    }
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            //若是e不爲空就替換舊的oldValue值
            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;
    }

 

注:hash 衝突發生的幾種狀況:
1.兩節點key 值相同(hash值必定相同),致使衝突;
2.兩節點key 值不一樣,因爲 hash 函數的侷限性致使hash 值相同,衝突;
3.兩節點key 值不一樣,hash 值不一樣,但 hash 值對數組長度取模後相同,衝突;

 

相比put方法,get方法就比較簡單,這裏就不說了。

 

1.7和1.8的HashMap的不一樣點

 

一、JDK1.7用的是頭插法,而JDK1.8及以後使用的都是尾插法,那麼爲何要這樣作呢?由於JDK1.7是用單鏈表進行的縱向延伸,當採用頭插法就是可以提升插入的效率,可是也會容易出現逆序且環形鏈表死循環問題。可是在JDK1.8以後是由於加入了紅黑樹使用尾插法,可以避免出現逆序且鏈表死循環的問題。

 

二、擴容後數據存儲位置的計算方式也不同:

 

  • 在JDK1.7的時候是直接用hash值和須要擴容的二進制數進行&(這裏就是爲何擴容的時候爲啥必定必須是2的多少次冪的緣由所在,由於若是隻有2的n次冪的狀況時最後一位二進制數才必定是1,這樣能最大程度減小hash碰撞)(hash值 & length-1) 。

     

  • 而在JDK1.8的時候直接用了JDK1.7的時候計算的規律,也就是擴容前的原始位置+擴容的大小值=JDK1.8的計算方式,而再也不是JDK1.7的那種異或的方法。可是這種方式就至關於只須要判斷Hash值的新增參與運算的位是0仍是1就直接迅速計算出了擴容後的儲存方式。

     

三、JDK1.7的時候使用的是數組+ 單鏈表的數據結構。可是在JDK1.8及以後時,使用的是數組+鏈表+紅黑樹的數據結構(當鏈表的深度達到8的時候,也就是默認閾值,就會自動擴容把鏈表轉成紅黑樹的數據結構來把時間複雜度從O(N)變成O(logN)提升了效率)。

 

HashMap爲何是線程不安全的?


 

HashMap 在併發時可能出現的問題主要是兩方面:

 

put的時候致使的多線程數據不一致


好比有兩個線程A和B,首先A但願插入一個key-value對到HashMap中,首先計算記錄所要落到的 hash桶的索引座標,而後獲取到該桶裏面的鏈表頭結點,此時線程A的時間片用完了,而此時線程B被調度得以執行,和線程A同樣執行,只不過線程B成功將記錄插到了桶裏面,假設線程A插入的記錄計算出來的 hash桶索引和線程B要插入的記錄計算出來的 hash桶索引是同樣的,

 

那麼當線程B成功插入以後,線程A再次被調度運行時,它依然持有過時的鏈表頭可是它對此一無所知,以致於它認爲它應該這樣作,如此一來就覆蓋了線程B插入的記錄,這樣線程B插入的記錄就憑空消失了,形成了數據不一致的行爲。

 

resize而引發死循環


這種狀況發生在HashMap自動擴容時,當2個線程同時檢測到元素個數超過 數組大小 × 負載因子。

 

此時2個線程會在put()方法中調用了resize(),兩個線程同時修改一個鏈表結構會產生一個循環鏈表(JDK1.7中,會出現resize先後元素順序倒置的狀況)。接下來再想經過get()獲取某一個元素,就會出現死循環。

 

HashMap和HashTable的區別


 

HashMap和Hashtable都實現了Map接口,但決定用哪個以前先要弄清楚它們之間的分別。主要的區別有:線程安全性,同步(synchronization),以及速度。

 

  • HashMap幾乎能夠等價於Hashtable,除了HashMap是非synchronized的,並能夠接受null(HashMap能夠接受爲null的鍵值(key)和值(value),而Hashtable則不行)。

     

  • HashMap是非synchronized,而Hashtable是synchronized,這意味着Hashtable是線程安全的,多個線程能夠共享一個Hashtable;而若是沒有正確的同步的話,多個線程是不能共享HashMap的。Java 5提供了ConcurrentHashMap,它是HashTable的替代,比HashTable的擴展性更好。

     

  • 另外一個區別是HashMap的迭代器(Iterator)是fail-fast迭代器,而Hashtable的enumerator迭代器不是fail-fast的。因此當有其它線程改變了HashMap的結構(增長或者移除元素),將會拋出ConcurrentModificationException,但迭代器自己的remove()方法移除元素則不會拋出ConcurrentModificationException異常。但這並非一個必定發生的行爲,要看JVM。這條一樣也是Enumeration和Iterator的區別。

     

  • 因爲Hashtable是線程安全的也是synchronized,因此在單線程環境下它比HashMap要慢。若是你不須要同步,只須要單一線程,那麼使用HashMap性能要好過Hashtable。

     

  • HashMap不能保證隨着時間的推移Map中的元素次序是不變的。

 

須要注意的重要術語:

 

  • sychronized意味着在一次僅有一個線程可以更改Hashtable。就是說任何線程要更新Hashtable時要首先得到同步鎖,其它線程要等到同步鎖被釋放以後才能再次得到同步鎖更新Hashtable。

     

  • Fail-safe和iterator迭代器相關。若是某個集合對象建立了Iterator或者ListIterator,而後其它的線程試圖「結構上」更改集合對象,將會拋出ConcurrentModificationException異常。但其它線程能夠經過set()方法更改集合對象是容許的,由於這並無從「結構上」更改集合。可是假如已經從結構上進行了更改,再調用set()方法,將會拋出IllegalArgumentException異常。

     

  • 結構上的更改指的是刪除或者插入一個元素,這樣會影響到map的結構。

 

HashMap能夠經過下面的語句進行同步:

Map m = Collections.synchronizeMap(hashMap);

 

watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=

相關文章
相關標籤/搜索