關於Java你不知道的那些事之Java8新特性[HashMap優化]
Java8新特性[HashMap優化]
前言
本文開始重溫Java8新特性之HashMap優化,後續還會重溫其他主要新特性,敬請期待,點點關注不迷路哦!!
其他主要新特性
- Lambda表示式
- 函式式介面
- 方法引用與構造器引用
- Stream API
- 介面中預設方法與靜態方法
- 新時間日期API
- 最大化減少空指標異常(Optional)
- 。。。。
HashMap優化
HashMap1.7
在JDK1.7 到 JDK1.8的時候,對HashMap做了優化
首先JDK1.7的HashMap當出現Hash碰撞的時候,最後插入的元素會放在前面,這個稱為 “頭插法”
JDK7用頭插是考慮到了一個所謂的熱點資料的點(新插入的資料可能會更早用到),但這其實是個偽命題,因為JDK7中rehash的時候,舊連結串列遷移新連結串列的時候,如果在新表的陣列索引位置相同,則連結串列元素會倒置(就是因為頭插) 所以最後的結果 還是打亂了插入的順序 所以總的來看支撐JDK7使用頭插的這點原因也不足以支撐下去了 所以就乾脆換成尾插 一舉多得
HashMap1.7存在死鏈問題
在JDK1.8以後,由頭插法改成了尾插法,因為頭插法還存在一個死鏈的問題
在說死鏈問題時,我們先從Hashmap儲存資料說起,下面這個是HashMap的put方法
public V put(K key, V value)
{
......
//計算Hash值
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;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
//該key不存在,需要增加一個結點
addEntry(hash, key, value, i);
return null;
}
這裡新增一個節點需要檢查是否超出容量,出現一個負載因子
void addEntry(int hash, K key, V value, int bucketIndex)
{
Entry<K,V> e = table[bucketIndex];
table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
//檢視當前的size是否超過了我們設定的閾值threshold,如果超過,需要resize
if (size++ >= threshold)
resize(2 * table.length);//擴容都是2倍2倍的來的,
}
HashMap有 負載因子:0.75,以及 初始容量:16,擴容閾值:16*0.75 = 12,當HashMap達到擴容的條件時候,會把HashMap中的每個元素,重新進行運算Hash值,打入到擴容後的陣列中。
既然新建了一個更大尺寸的hash表,然後把資料從老的Hash表中遷移到新的Hash表中。
void resize(int newCapacity)
{
Entry[] oldTable = table;
int oldCapacity = oldTable.length;
......
//建立一個新的Hash Table
Entry[] newTable = new Entry[newCapacity];
//將Old Hash Table上的資料遷移到New Hash Table上
transfer(newTable);
table = newTable;
threshold = (int)(newCapacity * loadFactor);
}
重點在這個transfer()方法
void transfer(Entry[] newTable)
{
Entry[] src = table;
int newCapacity = newTable.length;
//下面這段程式碼的意思是:
// 從OldTable裡摘一個元素出來,然後放到NewTable中
for (int j = 0; j < src.length; j++) {
Entry<K,V> e = src[j];
if (e != null) {
src[j] = null;
do {
Entry<K,V> next = e.next;
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
e = next;
} while (e != null);
}
}
}
do迴圈裡面的是最能說明問題的,當只有一個執行緒的時候:
最上面的是old hash 表,其中的Hash表的size=2, 所以key = 3, 7, 5,在mod 2以後都衝突在table[1]這裡了。接下來的三個步驟是Hash表 擴容變成4,然後在把所有的元素放入新表
do {
Entry<K,V> next = e.next; // <--假設執行緒一執行到這裡就被排程掛起了
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
e = next;
} while (e != null);
而我們的執行緒二執行完成了。於是我們有下面的這個樣子
注意,因為Thread1的 e 指向了key(3),而next指向了key(7),其線上程二rehash後,指向了執行緒二重組後的連結串列。我們可以看到連結串列的順序被反轉後。
這裡的意思是執行緒1這會還沒有完全開始擴容,但e和next已經指向了,執行緒2是正常的擴容的,那這會在3這個位置上,就是7->3這個順序。
然後執行緒一被排程回來執行:
先是執行 newTalbe[i] = e;
然後是e = next,導致了e指向了key(7),
而下一次迴圈的next = e.next導致了next指向了key(3)
注意看圖裡面的線,執行緒1指向執行緒2裡面的key3.
執行緒一接著工作。把key(7)摘下來,放到newTable[i]的第一個,然後把e和next往下移。
這時候,原來的執行緒2裡面的key7的e和key3的next沒了,e=key3,next=null。
當繼續執行,需要將key3加回到key7的前面。
e.next = newTable[i] 導致 key(3).next 指向了 key(7)
注意:此時的key(7).next 已經指向了key(3), 環形連結串列就這樣出現了。
執行緒2生成的e和next的關係影響到了執行緒1裡面的情況。從而打亂了正常的e和next的鏈。於是,當我們的執行緒一呼叫到,HashTable.get(11)時,即又到了3這個位置,需要插入新的,那這會就e 和next就亂了
HashMap每次擴容為什麼是2倍
首先看向HashMap中新增元素是怎麼存放的
第一個截圖是向HashMap中新增元素putVal()方法的部分原始碼,可以看出,向集合中新增元素時,會使用(n - 1) & hash的計算方法來得出該元素在集合中的位置;而第二個截圖是HashMap擴容時呼叫resize()方法中的部分原始碼,可以看出會新建一個tab,然後遍歷舊的tab,將舊的元素進過e.hash & (newCap - 1)的計算新增進新的tab中,也就是(n - 1) & hash的計算方法,其中n是集合的容量,hash是新增的元素進過hash函式計算出來的hash值
HashMap的容量為什麼是2的n次冪,和這個(n - 1) & hash的計算方法有著千絲萬縷的關係,符號&是按位與的計算,這是位運算,計算機能直接運算,特別高效,按位與&的計算方法是,只有當對應位置的資料都為1時,運算結果也為1,當HashMap的容量是2的n次冪時,(n-1)的2進位制也就是1111111***111這樣形式的,這樣與新增元素的hash值進行位運算時,能夠充分的雜湊,使得新增的元素均勻分佈在HashMap的每個位置上,減少hash碰撞,下面舉例進行說明。
當HashMap的容量是16時,它的二進位制是10000,(n-1)的二進位制是01111,與hash值得計算結果如下:
上面四種情況我們可以看出,不同的hash值,和(n-1)進行位運算後,能夠得出不同的值,使得新增的元素能夠均勻分佈在集合中不同的位置上,避免hash碰撞,下面就來看一下HashMap的容量不是2的n次冪的情況,當容量為10時,二進位制為01010,(n-1)的二進位制是01001,向裡面新增同樣的元素,結果為:
可以看出,有三個不同的元素進過&運算得出了同樣的結果,嚴重的hash碰撞了。
終上所述,HashMap計算新增元素的位置時,使用的位運算,這是特別高效的運算;另外,HashMap的初始容量是2的n次冪,擴容也是2倍的形式進行擴容,是因為容量是2的n次冪,可以使得新增的元素均勻分佈在HashMap中的陣列上,減少hash碰撞,避免形成連結串列的結構,使得查詢效率降低
JDK1.8結構變化
由JDK1.7的,陣列 + 連結串列
JDK1.8變為:陣列 + 連結串列 + 紅黑樹
具體觸發條件為:某個連結串列連線的個數大於8,並且總的容量大於64的時候,那麼會把原來的連結串列轉換成紅黑樹
這麼做的好處是什麼:除了新增元素外,查詢和刪除效率比連結串列快
紅黑樹查詢、增加和刪除的時間複雜度:O(log2n)
連結串列的查詢和刪除的時間複雜度: O(n),插入為:O(1)
ConcurrentHashMap變化
為何JDK8要放棄分段鎖?
由原來的分段鎖,變成了CAS,也就是通過無鎖化設計替代了阻塞同步的加鎖操作,效能得到了提高。
通過分段鎖的方式提高了併發度。分段是一開始就確定的了,後期不能再進行擴容的,其中的段Segment繼承了重入鎖ReentrantLock,有了鎖的功能,同時含有類似HashMap中的陣列加連結串列結構(這裡沒有使用紅黑樹),雖然Segment的個數是不能擴容的,但是單個Segment裡面的陣列是可以擴容的。
JDK1.8的ConcurrentHashMap摒棄了1.7的segment設計,而是JDK1.8版本的HashMap的基礎上實現了執行緒安全的版本,即也是採用陣列+連結串列+紅黑樹的形式,雖然ConcurrentHashMap的讀不需要鎖,但是需要保證能讀到最新資料,所以必須加volatile。即陣列的引用需要加volatile,同時一個Node節點中的val和next屬性也必須要加volatile。
至於為什麼拋棄Segment的設計,是因為分段鎖的這個段不太好評定,如果我們的Segment設定的過大,那麼隔離級別也就過高,那麼就有很多空間被浪費了,也就是會讓某些段裡面沒有元素,如果太小容易造成衝突
記憶體結構優化
取消永久區,把方法區 放在 元空間中
方法區主要用於儲存一些類别範本
OOM錯誤發生概率降低
同時相關JVM調優命令變為:
MetaspaceSize
MaxMetaspaceSize
gment設定的過大,那麼隔離級別也就過高,那麼就有很多空間被浪費了,也就是會讓某些段裡面沒有元素,如果太小容易造成衝突
總結
點贊+關注,謝謝
相關文章
- 關於Java序列化你不知道的事Java
- 【Java8新特性】關於Java8中的日期時間API,你需要掌握這些!!JavaAPI
- Java8新特性之:OptionalJava
- 關於執行緒池,那些你還不知道的事執行緒
- java8 之 Java官方庫的新特性Java
- ?Java8新特性之Optional類Java
- java8 新特性之方法引用Java
- java8 新特性之Optional 類Java
- Java8 新特性之 Optional 類Java
- Java8的新特性Java
- JAVA8新特性Java
- Java8 新特性Java
- java8 新特性之Lambda 表示式Java
- java8 新特性之預設方法Java
- Java8新特性探索之Stream介面Java
- Java8 新特性之 Lambda 表示式Java
- Java8新特性之時間APIJavaAPI
- 【Java8新特性】關於Java8的Stream API,看這一篇就夠了!!JavaAPI
- 【Bugly乾貨】關於 Android N 那些你不知道的事兒Android
- 關於Docker你不知道的事——虛擬化歷史Docker
- java8 新特性之函式式介面Java函式
- java8新特性之lambda表示式(一)Java
- Java8 新特性之預設介面方法Java
- Java8新特性 - LambdaJava
- JAVA8新特性用法Java
- 【Java8新特性】冰河帶你看盡Java8新特性,你想要的都在這兒了!!(文字有福利)Java
- 你所不知道的Java效能優化之String!Java優化
- Java String之你不知道的事Java
- Java8的八個新特性Java
- Java8新特性探索之函式式介面Java函式
- 關於webpack優化,你需要知道的事(上篇)Web優化
- Java8 新特性,打破你對介面的認知Java
- Java8新特性,你應該瞭解這些!Java
- Java8新特性--Stream APIJavaAPI
- java8新特性stream流Java
- Java8新特性實踐Java
- Java8新特性系列-LambdaJava
- Java8新特性系列(Stream)Java