HashMap調優和ConcurrentHashMap分析
阿新 • • 發佈:2019-01-27
之前談到了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 這樣的類作為鍵,原因如下:
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