1. 程式人生 > >HashMap調優和ConcurrentHashMap分析

HashMap調優和ConcurrentHashMap分析

之前談到了HashMap的存和取,這次來聊一下它的調優,以及多執行緒下的不用HashMap轉用ConcurrentHashMap的一點淺析

重述HashMap工作原理:

  • HashMap是基於hash原理,我們使用put()儲存物件,使用get()獲取物件
  • 當我們給put方法傳鍵值時,他會先呼叫hashCode方法,用於查詢鍵值在 bucket的位置,進而儲存物件的鍵值對
  • 當兩個物件的hashCode相同,在儲存時候就會發生碰撞,原因就是HashMap採取整合Map和連結串列的儲存方式,繼而呼叫equals比較,沒有就存進去,有就把之前的替換掉

HashMap調優:

先貼出HashMap原始碼普及一下幾個概念:
public class HashMap<K,V>extends AbstractMap<K,V>implements Map<K,V>, Cloneable, Serializable
{
    //  預設的初始容量(容量為HashMap中桶的數目)是16,且實際容量必須是2的整數次冪。 
    static final int DEFAULT_INITIAL_CAPACITY = 16;
// 最大容量(必須是2的冪且小於2的30次方,傳入容量過大將被這個值替換) static final int MAXIMUM_CAPACITY = 1 << 30; // 預設載入因子 static final float DEFAULT_LOAD_FACTOR = 0.75f; // 儲存資料的Entry陣列,長度是2的冪。 // HashMap是採用拉鍊法實現的,每一個Entry本質上是一個單向連結串列 transient Entry[] table; // HashMap的大小,它是HashMap儲存的鍵值對的數量 transient int size; // HashMap的閾值,用於判斷是否需要調整HashMap的容量(threshold = 容量*載入因子) int threshold;
// 載入因子實際大小 final float loadFactor; // HashMap被改變的次數 transient volatile int modCount;
通過以上原始碼可以看到在原始碼中定義了一下幾個常量:
  • 預設載入因子:這東西說白了就是用來劃分整個HashMap容量的百分比,這裡預設0.75就是說佔用總容量的75%
  • 預設初始容量:如果你不在建構函式中傳值,new一個HashMap,他的容量就是2的4次方(16),並且增長也得是2的整數次方(冪)
  • 閥值:首先這個值等於預設載入因子和初始容量的乘機;他的作用是用來預警的,如果HashMap中的容量超過這個閥值了,那就會執行擴容操作,低於則沒事

容量調優:

如果你要在HashMap中存20個元素,他預設只有16 當你儲存到13時候就會執行擴容(rehashing)這個是很費資源的操作,並且還會出現死迴圈,建議你在知道你要儲存的容量的時候,直接這樣定義:  
Map mapBest = new HashMap((int) ((float) 擬存的元素個數 / 0.75F + 1.0F));

這樣一次到位,雖然存在些資源浪費,但是比起重新擴容還是效率高很多

減小負載因子:

  • 首先這個負載因子不建議定義成比0.75 大了,因為如果等到沒有空間了再分配可能丟擲error
  • 但是也不建議吧負載因子調的過低,造成資源大面積浪費
  • 在建構函式裡,設定載入因子是0.5甚至0.25。如果你的Map是一個長期存在而不是每次動態生成的,而裡面的key又是沒法預估的,那可以適當加大初始大小,同時減少載入因子,降低衝突的機率。畢竟如果是長期存在的map,浪費點陣列大小不算啥,降低衝突概率,減少比較的次數更重要。

優化Key設計:

看一下獲取key對應value的原始碼
    // 獲取key對應的value
    public V get(Object key) {
        if (key == null)
            return getForNullKey();
        // 獲取key的hash值
        int hash = hash(key.hashCode());
        // 在“該hash值對應的連結串列”上查詢“鍵值等於key”的元素
        for (Entry<K,V> e = table[indexFor(hash, table.length)];
             e != null;
             e = e.next) {
            Object k;
            if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
                return e.value;
        }
        return null;
    }

由原始碼可知,如果hashCode 不衝突,那查詢效率很高,但是如果hashCode一旦衝突,叫呼叫equals一個位元組一個自己的去比較
  • 所以你把key設計的儘量短,一旦衝突也會少用點時間
  • 建議採用String,Integer 這樣的類作為鍵,原因如下:
特別是String,他是不可變的,也是final的,而且已經重寫了equals 和hashCode 方法,這個和HashMap 要求的計算hashCode的不可變性要求不謀而合,核心思想就是保證鍵值的唯一性,不變性, 其次是不可變性還有諸如執行緒安全的問題,以上這麼定義鍵,可以最大限度的減少碰撞的出現

Hash攻擊:

HashMap中當呼叫HashCode 方法時,如果值相同就會存在碰撞,攻擊者利用不同輸入會產生相同HashCode 的漏洞進行緩慢攻擊,等到碰撞得到一定程度,cpu會拿出打分開銷開處理碰撞,這時候服務可能宕機 這就是Hash攻擊 具體的例如String 轉Json就用到了HashMap ,但是這個情況 在Java8中有鎖改善

多執行緒下的選擇:

HashMap 缺點:

看下HashMap put方法的原始碼:
// 將“key-value”新增到HashMap中
    public V put(K key, V value) {
        // 若“key為null”,則將該鍵值對新增到table[0]中。
        if (key == null)
            return putForNullKey(value);
        // 若“key不為null”,則計算該key的雜湊值,然後將其新增到該雜湊值對應的連結串列中。
        int hash = hash(key.hashCode());
        int i = indexFor(hash, table.length);
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
            Object k;
            // 若“該key”對應的鍵值對已經存在,則用新的value取代舊的value。然後退出!
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }
        // 若“該key”對應的鍵值對不存在,則將“key-value”新增到table中
        modCount++;
        addEntry(hash, key, value, i);
        return null;
    }
HashMap 在併發執行put操作的時候會引起死迴圈,是因為多執行緒會導致hashMap的Entry 連結串列形成喚醒資料結構,一旦形成喚醒的資料結構,Entry的next節點永遠不為空,就會產生死迴圈獲取Entry

多執行緒下的HashTable的缺點:

HashTable使用synchronized來保證執行緒安全,但是線上程競爭激烈的情況下,當一個執行緒訪問同步方法的時候,其餘的執行緒會被阻塞或者輪詢狀態,就這樣乾等著,啥也幹不了,效率低的不行

多執行緒下的選擇ConcurrentHashMap:

ConcurrentHashMap 採取鎖分段技術,將資料分成一段一段地儲存,然後把每一段資料配置一把鎖,當一個執行緒佔用鎖訪問其中的一段資料的時候,其他的斷的資料也能被其他執行緒訪問 以上是針對HashMap 的分析,關於ConcurrentHashMap  等多執行緒的部分 請見下回分解