HashMap的底層數據結構?
數組+鏈表 , 數組+鏈表+紅黑樹
HashMap的存取原理?
通過獲取key對象的hashcode計算出該對象的哈希值,通過改哈希值與數組長度減去1進行位與運算(n-1 & hash),得到buckets 的位置,當發生hash沖突時,如果value值一樣,則會替換舊的key的value,value不一樣則新建鏈表結點,當鏈表的長度超過8,則轉換為紅黑樹存儲。
Java7和Java8的區別?
在jdk1.8之前創建該對象,會創建一個長度為16的Entry[] table用來存儲鍵值對數據。jdk1.8之后不是在構造方法創建了,而是在第一次調用put方法時才進行創建,創建Node[] table,然后java7中鏈表的加入時
Java7在多線程操作HashMap時可能引起死循環,原因是擴容轉移后前后鏈表順序倒置,在轉移過程中修改了原來鏈表中節點的引用關系。
為啥會線程不安全?
Java7在多線程操作HashMap時可能引起死循環,原因是擴容轉移后前后鏈表順序倒置,在轉移過程中修改了原來鏈表中節點的引用關系。
Java8在同樣的前提下並不會引起死循環,原因是擴容轉移后前后鏈表順序不變,保持之前節點的引用關系。
但是即使不會出現死循環,但是通過源碼看到put/get方法都沒有加同步鎖,多線程情況最容易出現的就是:無法保證上一秒put的值,下一秒get的時候還是原值,所以線程安全還是無法保證。
HashMap會進行resize(擴容)操作,重新計算hash值,在resize操作的時候會造成線程不安全。下面將舉兩個可能出現線程不安全的地方。
1、put的時候導致的多線程數據不一致。
這個問題比較好想象,比如有兩個線程A和B,首先A希望插入一個key-value對到HashMap中,首先計算記錄所要落到的桶的索引坐標,然后獲取到該桶里面的鏈表頭結點,此時線程A的時間片用完了,而此時線程B被調度得以執行,和線程A一樣執行,只不過線程B成功將記錄插到了桶里面,假設線程A插入的記錄計算出來的桶索引和線程B要插入的記錄計算出來的桶索引是一樣的,那么當線程B成功插入之后,線程A再次被調度運行時,它依然持有過期的鏈表頭但是它對此一無所知,以至於它認為它應該這樣做,如此一來就覆蓋了線程B插入的記錄,這樣線程B插入的記錄就憑空消失了,造成了數據不一致的行為。
2、另外一個比較明顯的線程不安全的問題是HashMap的get操作可能因為resize而引起死循環(cpu100%),即產生鏈表循環引用的現象(jdk7)
有什么線程安全的類代替么?
currentHashMap 以及 hashTable
默認初始化大小是多少?為啥是這么多?為啥大小都是2的冪?
默認的初始化大小是16 原因是這樣的,如果桶初始化桶數組設置太大,就會浪費內存空間,16是一個折中的大小,既不會像1,2,3那樣放幾個元素就擴容,也不會像幾千幾萬那樣可以只會利用一點點空間從而造成大量的浪費。
大小為2的冪是,在計算buckets桶位置的時候,公式為((n-1) & hash),2的冪減去1的數的二進制數的結尾都是1,與hash值進行與運算,會得到其余數。進行按位與操作,使得結果剩下的值為對象的hash值的末尾幾位,這樣就我們只要保證對象的hash值生成足夠散列即可
使存儲高效,盡量減少碰撞,在((n-1)&hash) 求索引的時候更均勻
數組長度是2的n次冪時
數組長度 不是2的n次冪時
HashMap的擴容方式?負載因子是多少?為什是這么多?
加載因子設置為0.75而不是1,是因為設置過大,桶中鍵值對碰撞的幾率就會越大,同一個桶位置可能會存放好幾個value值,這樣就會增加搜索的時間,性能下降,設置過小也不合適,如果是0.1,那么10個桶,threshold為1,你放兩個鍵值對就要擴容,太浪費空間了。
HashMap的主要參數都有哪些?
//默認的map大小,為2的n次冪
static final int DEFAULT_INITIAL_CAPACITY(默認初始容量) = 1 << 4; // aka 16
// 最大容量,指定的值超過 2的30次冪,默認使用這個值
static final int MAXIMUM_CAPACITY (最大容量)= 1 << 30;
//在構造函數中沒有指定時使用的負載因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//當鏈表長度為8時,使用以紅黑樹代替鏈表,紅黑樹的結點是鏈表長度的兩倍,當比較短的時候,使用紅黑樹的效率其實並不高,根據泊松分布公式的統計結果,在結點數達到8時,適合使用紅黑樹
static final int TREEIFY_THRESHOLD(恐嚇的閾值) = 8;
// 紅黑樹轉為鏈表的閾值
static final int UNTREEIFY_THRESHOLD (非恐嚇的閾值)= 6;
//鏈表轉紅黑樹時數組的大小的閾值,即數組大小大於這個數字時且鏈表長度大於8才會轉為紅黑樹,
//在數組長度小於64,不會轉,而是進行擴容
static final int MIN_TREEIFY_CAPACITY = 64(小的恐嚇容量);
HashMap是怎么處理hash碰撞的?
如果key相同,則會替換key對應的內容最最小值,key不相同,則接到后面的鏈表,如果鏈表長度到達8且數組的長度大於64時,則將鏈表轉為紅黑樹,如果數組長度小於64,則是進行擴容
hash的計算規則?
將對象的hashcode()方法返回的hash值,進行無符號的右移16位,並與原來的hash值進行按位異或操作,目的是將hash的低16bit和高16bit做了一個異或,使返回的值足夠散列
在get和put的過程中,計算下標時,先對hashCode進行hash操作,然后再通過hash值進一步計算下標,如下圖所示:
如何解決初始化,輸入的值不是2的n次冪
/**
* Returns a power of two size for the given target capacity.
*/
static final int tableSizeFor(int cap) {
int n = cap - 1;
n |= n >>> 1
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
目的:將值的二進制數,從左到右的第一個出現的1開始,右邊的所有值都變成1,使得可以找出比當前值大一點點的2的n次冪的數
當執行 n >>> 16 時,即意味着將32位數都進行了一次按位或運算,將