鏈表轉紅黑樹的原因?為什么閾值為8?


為什么 Map 桶中超過 8 個才轉為紅黑樹?

我們知道Java8后,當Map鏈表長度大於或等於閾值(默認為 8)的時候,如果同時還滿足容量大於或等於 MIN_TREEIFY_CAPACITY(默認為 64)的要求,就會把鏈表轉換為紅黑樹。同樣,后續如果由於刪除或者其他原因調整了大小,當紅黑樹的節點小於或等於 6 個以后,又會恢復為鏈表形態。

首先要知道為什么要轉換為紅黑樹?因為轉換是第一步

每次遍歷一個鏈表,平均查找的時間復雜度是 O(n),n 是鏈表的長度。紅黑樹有和鏈表不一樣的查找性能,由於紅黑樹有自平衡的特點,可以防止不平衡情況的發生,所以可以始終將查找的時間復雜度控制在 O(log(n))。最初鏈表還不是很長,所以可能 O(n) 和 O(log(n)) 的區別不大,但是如果鏈表越來越長,那么這種區別便會有所體現。所以為了提升查找性能,需要把鏈表轉化為紅黑樹的形式。

那為什么不一開始就用紅黑樹,反而要經歷一個轉換的過程呢?

其實在 JDK 的源碼注釋中已經對這個問題作了解釋:

這段話的意思是:單個 TreeNode 需要占用的空間大約是普通 Node 的兩倍,所以只有當包含足夠多的 Nodes 時才會轉成 TreeNodes,而是否足夠多就是由 TREEIFY_THRESHOLD 的值決定的。而當桶中節點數由於移除或者 resize 變少后,又會變回普通的鏈表的形式,以便節省空間。

從鏈表轉化為紅黑樹的閾值為什么是8?

通過查看源碼可以發現,默認是鏈表長度達到 8 就轉成紅黑樹,而當長度降到 6 就轉換回去,這體現了時間和空間平衡的思想,最開始使用鏈表的時候,空間占用是比較少的,而且由於鏈表短,所以查詢時間也沒有太大的問題。可是當鏈表越來越長,需要用紅黑樹的形式來保證查詢的效率。對於何時應該從鏈表轉化為紅黑樹,需要確定一個閾值,這個閾值默認為 8,並且在源碼中也對選擇 8 這個數字做了說明,原文如下:

 上面這段話的意思是,如果 hashCode 分布良好,也就是 hash 計算的結果離散好的話,那么紅黑樹這種形式是很少會被用到的,因為各個值都均勻分布,很少出現鏈表很長的情況。在理想情況下,鏈表長度符合泊松分布,各個長度的命中概率依次遞減,當長度為 8 的時候,概率僅為 0.00000006。這是一個小於千萬分之一的概率,通常我們的 Map 里面是不會存儲這么多的數據的,所以通常情況下,並不會發生從鏈表向紅黑樹的轉換。

但是,HashMap 決定某一個元素落到哪一個桶里,是和這個對象的 hashCode 有關的,JDK 並不能阻止我們用戶實現自己的哈希算法,如果我們故意把哈希算法變得不均勻,例如:

事實上,鏈表長度超過 8 就轉為紅黑樹的設計,更多的是為了防止用戶自己實現了不好的哈希算法時導致鏈表過長,從而導致查詢效率低,而此時轉為紅黑樹更多的是一種保底策略,用來保證極端情況下查詢的效率。

通常如果 hash 算法正常的話,那么鏈表的長度也不會很長,那么紅黑樹也不會帶來明顯的查詢時間上的優勢,反而會增加空間負擔。所以通常情況下,並沒有必要轉為紅黑樹,所以就選擇了概率非常小,小於千萬分之一概率,也就是長度為 8 的概率,把長度 8 作為轉化的默認閾值。

所以如果平時開發中發現 HashMap 或是 ConcurrentHashMap 內部出現了紅黑樹的結構,這個時候往往就說明我們的哈希算法出了問題,需要留意是不是我們實現了效果不好的 hashCode 方法,並對此進行改進,以便減少沖突。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM