1. 程式人生 > >java8的hashmap

java8的hashmap

HASHMAP

JAVA8對HashMap的調整在兩個方面,一是當連結串列中的元素超過了 8 個以後,會將連結串列轉換為紅黑樹

二是新的鍵值對會插入到連結串列尾部而不是頭部。

 

HASHMAP

 

    Treemap和LinkedHashMap如何保證順序的?

        Treemap通過實現SortMap介面,能夠把它儲存的鍵值對根據key排序,基於紅黑樹,從而保證Treemap中所有鍵值對處於有序狀態;

        LinkedHashMap則是通過插入排序(put的時候順序是什麼,取出來的就是什麼順序)和訪問排序(改變順序把訪問過的放到底部)讓鍵值有序

Hashtable為什麼不能接受null建和值?

        因為equals()方法需要物件,不能為空

減少碰撞的方法?

    擾動函式;原理:如果兩個不相等的物件返回不同的hashcode,那麼碰撞的機率就會小,這意味著存連結串列結構減小,取值時就不用頻繁呼叫equals方法,提高HashMap的效能(擾動即Hash方法內部的演算法實現,目的是讓不同物件返回不同的hashcode)

    使用不可變,宣告作final的物件,並且採用合適的equals()和hashCode()方法的話,將會減少碰撞的發生。不可變效能夠快取不同建的hashcode,將提高整個獲取物件的速度。

為什麼選擇紅黑樹而不用二叉查詢樹?

    二叉查詢樹在特殊情況下回變成線性結構;引入紅黑樹是為了查詢資料快,解決連結串列查詢深度的問題;

紅黑樹?

    1.每個節點非黑即紅;2.根節點總是黑色的;3.如果節點是紅色的,則它的子節點是黑色的(反之不一一定);4.每個葉子節點都是黑色的空節點(NIL節點);5.從根節點到業績誒單或空子節點的每條路徑,必須包含相同資料的黑色節點(既相同的黑色高度)

 

    hshtable預設容量:11

    ConcurrentHashMap原理: 引入了CAS(記憶體值和預期值相同時,才會更改為新值)對sizeCtl的控制都是由CAS來實現的。預設為0;

        對變數增加一個版本號,每次修改,版本號+1,比較的時候比較版本號;

    ConcurrentHashMap同步效能更好,因為僅僅根據同步級別對map的一部分進行上鎖;

當HashTable的大小增加到一定程度時,效能下降,因為迭代時需要被鎖定很長時間。(鎖定整個map)

   而ConcurrentHashMap引入了分割,無論多大,僅僅需要鎖定map的某個部分,而其他的執行緒不需要等到迭代完整才能訪問map。

 

 

hashmap

  • 使用雜湊表(散列表)來進行資料儲存,並使用鏈地址法來解決衝突
  • 當連結串列長度大於等於 8 時,將連結串列轉換為紅黑樹來儲存
  • 每次進行二次冪的擴容,即擴容為原容量的兩倍

treemap

  • TreeMap 中儲存的記錄會根據 Key 排序(預設為升序排序),因此使用 Iterator 遍歷時得到的記錄是排過序的
  • 因為需要排序,所以TreeMap 中的 key 必須實現 Comparable 介面,否則會報 ClassCastException 異常
  • TreeMap 會按照其 key 的 compareTo 方法來判斷 key 是否重複

 

 hash(Object key)

HashMap 便是通過 hashCode 來確定一個 key 在陣列中的儲存位置。

HashMap 並非直接使用 hashCode 作為雜湊值,而是通過這裡的 hash 方法對 hashCode 進行一系列的移位和異或處理,這樣處理的目的是為了有效地避免雜湊碰撞

static final int hash(Object key) {

    int h;

    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);

}

put(K key, V value)

public V put(K key, V value) {

    return putVal(hash(key), key, value, false, true);

}

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    HashMap.Node<K, V>[] tab;
    HashMap.Node<K, V> p;
    int n, i;
    if ((tab = table) == null || (n = tab.length) == 0)
        //初始化雜湊表
        n = (tab = resize()).length;
    if ((p = tab[i = (n - 1) & hash]) == null)
        //通過雜湊值找到對應的位置,如果該位置還沒有元素存在,直接插入
        tab[i] = newNode(hash, key, value, null);
    else {
        HashMap.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 HashMap.TreeNode)
            //如果當前節點為樹節點,則將元素插入紅黑樹中
            e = ((HashMap.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);
                    if (binCount >= TREEIFY_THRESHOLD - 1)
                        //元素個數大於等於 8,改造為紅黑樹
                        treeifyBin(tab, hash);
                    break;
                }
                //如果該位置的元素的 key 與之相等,則重新賦值
                if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;
            }
        }
        //前面當雜湊表中存在當前key時對e進行了賦值,這裡統一對該key重新賦值更新
        if (e != null) { 
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }
    }
    ++modCount;
    //檢查是否超出 threshold 限制,是則進行擴容
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
}
 

 

 

在 Java 8 之前,HashMap 插入資料時一直是插入到連結串列表頭;而到了 Java 8 之後,則改為了尾部插入。至於頭插入有什麼缺點,其中一個就是在併發的情況下因為插入而進行擴容時可能會出現連結串列環而發生死迴圈;當然,HashMap 設計出來本身就不是用於併發的情況的。

 

連結串列中元素大於等於 8,這時有可能將連結串列改造為紅黑樹的資料結構??

MIN_TREEIFY_CAPACITY 規定了 HashMap 可以樹化的最小表容量為 64,這是為了避免在雜湊表建立初期,多個鍵值對恰好被放入了同一個連結串列中而導致不必要的轉化

HashMap 為什麼要進行樹化呢?我們都知道,連結串列的查詢效率大大低於陣列,而當過多的元素連成連結串列,會大大降低查詢存取的效能;同時,這也涉及到了一個安全問題,一些程式碼可以利用能夠造成雜湊衝突的資料對系統進行攻擊,這會導致服務端 CPU 被大量佔用。

 

resize()

resize方法的三個if-else if-else

1. 表項只有一個鍵值對時,針對新表計算新的索引位置並插入鍵值對

2. 表項節點是紅黑樹節點時(說明這個bin元素較多已經轉成紅黑樹了),呼叫split方法處理。

3. 表項節點包含多個鍵值對組成的連結串列時(拉鍊法)

第一種情況就是直接對新的陣列長度取模計算新索引,放到新陣列的相應位置,和jdk7一樣的。第二種情況是引入了紅黑樹後獨有的,通過呼叫一個split方法處理,關於這個方法一會再細說。第三種情況在jdk7裡沒引入樹時也有,但是jdk8裡對這種情況也做了演算法上的優化。

當要儲存的元素超過閾值時,則進行2倍擴容,將原來map中的資料再次重新求hash索引值插入新的陣列中

java8:

2、對key值得hash值和舊陣列大小進行&與運算,如果結果為0,索引位置不變,還是舊索引位置,不為0則表示需要移位,新位置為原先位置+舊陣列的小大(新陣列大小為舊陣列翻倍),效率比Java7高。額外提一點,Java的連結串列節點數超過8個時,會將連結串列轉化為紅黑樹,

get(Object key)

final HashMap.Node<K,V> getNode(int hash, Object key) {
    HashMap.Node<K,V>[] tab; HashMap.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 && ((k = first.key) == key || (key != null && key.equals(k))))
            return first;
        if ((e = first.next) != null) {
            //否則檢查是否為樹節點,則呼叫 getTreeNode 方法獲取樹節點
            if (first instanceof HashMap.TreeNode)
                return ((HashMap.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;
}
 

主要就四步:

  1. 雜湊表是否為空或者目標位置是否存在元素
  2. 是否為第一個元素
  3. 如果是樹節點,尋找目標樹節點
  4. 如果是連結串列結點,遍歷連結串列尋找目標結點

JDK7中HashMap採用的是位桶+連結串列的方式。而JDK8中採用的是位桶+連結串列/紅黑樹的方式,當某個位桶的連結串列的長度超過8的時候,這個連結串列就將轉換成紅黑樹。因為引入了樹, 到了8以後,就要判斷是連結串列還是樹,如果是連結串列,插入後還要判斷要不要轉化成樹。不過這些操作都是常量級別的,複雜度還是O(1)的,但是對整體效能提升非常大。

Java7中對於<key1,value1>的put方法實現相對比較簡單,首先根據 key1 的key值計算hash值,再根據該hash值與table的length確定該key所在的index,如果當前位置的Entry不為null,則在該Entry鏈中遍歷,如果找到hash值和key值都相同,則將值value覆蓋,返回oldValue;如果當前位置的Entry為null,則直接addEntry。

java8不是用紅黑樹來管理hashmap,而是在hash值相同的情況下(且重複數量大於8),用紅黑樹來管理資料。 紅黑樹相當於排序資料。可以自動的使用二分法進行定位。效能較高。這一點在hash不均勻並且元素個數很多的情況時,對hashmap的效能提升非常大

一般情況下,hash值做的比較好的話基本上用不到紅黑樹。

 

 

 

對執行緒安全性要求高的時候可以用同步包裝器(Collections.synchronizedMap())包裝一個執行緒安全的hashmap。通過這種方式實現執行緒安全,所有訪問的執行緒都必須競爭同一把鎖,不管是get還是put。好處是比較可靠,但代價就是效能會差一點。

ConcurrentHashmap通過分段鎖技術提高了併發的效能,首先將資料分成一段一段的儲存,然後給每一段資料配一把鎖,當一個執行緒佔用鎖訪問其中一個段資料的時候,其他段的資料也能被其他執行緒訪問。另外concurrenthashmap的get操作沒有鎖,是通過volatile關鍵字保證資料的記憶體可見性。所以效能提高很多。JDK8對ConcurrentHashmap也有了巨大的的升級,同樣底層引入了紅黑樹,並且摒棄segment方式,採用新的CAS演算法思路去實現執行緒安全,再次把ConcurrentHashmap的效能提升了一個臺階。

 

jdk7裡hashmap resize時對每個位桶的連結串列的處理方式(transfer方法),整體過程就是先新建兩倍的新陣列,然後遍歷舊陣列的每一個entry,直接重新計算新的索引位置然後頭插法往拉鍊裡填坑(這裡因為是新加入的元素插入到連結串列頭,所以順序會倒置,jdk8裡不會)

jdk8的程式碼裡是這麼處理的,把連結串列上的鍵值對按hash值分成lo和hi兩串,lo串的新索引位置與原先相同[原先位置j],hi串的新索引位置為[原先位置j+oldCap]。這麼做的原因是,我們使用的是2次冪的擴充套件(newCap是oldCap的兩倍),所以,元素的位置要麼是在原位置,要麼是在原位置再移動2次冪的位置,也就是原索引+oldCap。為啥?自己舉個例子就知道了。那怎麼判斷該假如lo串還是hi串?這個取決於 判斷條件if ((e.hash & oldCap) == 0),如果條件為真,加入lo串,條件為假,加入hi串。那這是為什麼?因為這個&運算其實相當於做了一個掩碼,檢視原來的hash值新增的那個bit是1還是0就好了,是0的話索引沒變,是1的話索引變成“原索引+oldCap”。

 https://www.jianshu.com/p/4177dc15d658  簡書的java8對hashmap的改進

 

https://my.oschina.net/xianggao/blog/393990

Java的文件說HashMap是非執行緒安全的,應該用ConcurrentHashMap。

hashmap用於單執行緒, 程式可能Hang在了HashMap.get()這個方法上, 佔了100%的CPU(增加 synchronized 。恢復正常)

 https://blog.csdn.net/u011535541/article/details/80217128  java 7,8 對hashmap的對比

 

hashmap的 擴容放值   http://www.iteye.com/topic/539465

  1. HashMap 在 new 後並不會立即分配bucket陣列,而是第一次 put 時初始化,類似 ArrayList 在第一次 add 時分配空間。
  2. HashMap 的 bucket 陣列大小一定是2的冪,如果 new 的時候指定了容量且不是2的冪,實際容量會是最接近(大於)指定容量的2的冪,比如 new HashMap<>(19),比19大且最接近的2的冪是32,實際容量就是32。
  3. HashMap 在 put 的元素數量大於 Capacity LoadFactor(預設16 0.75) 之後會進行擴容。
  4. JDK8在雜湊碰撞的連結串列長度達到TREEIFY_THRESHOLD(預設8)後,會把該連結串列轉變成樹結構,提高了效能。
  5. JDK8在 resize 的時候,通過巧妙的設計,減少了 rehash 的效能消耗。

 

 

 

  1. javahashmap條目   https://yq.aliyun.com/articles/225660?utm_content=m_32836

put函式的思路大致分以下幾步:

  1. 對key的hashCode()進行hash後計算陣列下標index;
  2. 如果當前陣列table為null,進行resize()初始化;
  3. 如果沒碰撞直接放到對應下標的bucket裡;
  4. 如果碰撞了,且節點已經存在,就替換掉 value;
  5. 如果碰撞後發現為樹結構,掛載到樹上。
  6. 如果碰撞後為連結串列,新增到連結串列尾,並判斷連結串列如果過長(大於等於TREEIFY_THRESHOLD,預設8),就把連結串列轉換成樹結構;
  7. 資料 put 後,如果資料量超過threshold,就要resize。

 

resize()用來第一次初始化,或者 put 之後資料超過了threshold後擴容

陣列下標計算: index = (table.length - 1) & hash  ,由於 table.length 也就是capacity 肯定是2的N次方,使用 & 位運算意味著只是多了最高位,這樣就不用重新計算 index,元素要麼在原位置,要麼在原位置+ oldCapacity。

如果增加的高位為0,resize 後 index 不變,

這個設計的巧妙之處在於,節省了一部分重新計算hash的時間,同時新增的一位為0或1的概率可以認為是均等的,所以在resize 的過程中就將原來碰撞的節點又均勻分佈到了兩個bucket裡。

 

hash

JKD7 中,bucket陣列下標也是按位與計算,但是 hash 函式與 JDK8稍有不同,

  • hash為了防止只有 hashCode() 的低 bit 位參與雜湊容易碰撞,也採用了位移異或,只不過不是高低16bit,而是如下程式碼中多次位移異或。
  • JKD7的 hash 中存在一個開關:hashSeed。開關開啟(hashSeed不為0)的時候,對 String 型別的key 採用sun.misc.Hashing.stringHash32的 hash 演算法;對非 String 型別的 key,多一次和hashSeed的異或,也可以一定程度上減少碰撞的概率。
  • JDK 7u40以後,hashSeed 被移除,在 JDK8中也沒有再採用,因為stringHash32()的演算法基於MurMur雜湊,其中hashSeed的產生使用了Romdum.nextInt()實現。Rondom.nextInt()使用AtomicLong,它的操作是CAS的(Compare And Swap)。這個CAS操作當有多個CPU核心時,會存在許多效能問題。因此,這個替代函式在多核處理器中表現出了糟糕的效能。

hashSeed 預設值是0,也就是預設關閉,任何數字與0異或不變。hashSeed 會在capacity發生變化的時候,通過initHashSeedAsNeeded()函式進行計算。當capacity大於設定值Holder.ALTERNATIVE_HASHING_THRESHOLD後,會通過sun.misc.Hashing.randomHashSeed產生hashSeed 值,這個設定值是通過 JVM的jdk.map.althashing.threshold引數來設定的,