java.util.HashMap是很常見(jiàn)的類,前段時(shí)間公司系統(tǒng)由于對(duì)HashMap使用不當(dāng),導(dǎo)致cpu百分之百,在并發(fā)環(huán)境下使用HashMap而沒(méi)有做同步,可能會(huì)引起死循環(huán),關(guān)于這一點(diǎn),sun的官方網(wǎng)站上已有闡述,這并非是bug。 HashMap的數(shù)據(jù)結(jié)構(gòu) HashMap主要是用數(shù)組來(lái)存儲(chǔ)數(shù)據(jù)的,我們都知道它會(huì)對(duì)key進(jìn)行哈希運(yùn)算,哈系運(yùn)算會(huì)有重復(fù)的哈希值,對(duì)于哈希值的沖突,HashMap采用鏈表來(lái)解決的。在HashMap里有這樣的一句屬性聲明: transient Entry[] table; Entry就是HashMap存儲(chǔ)的數(shù)據(jù),它擁有的屬性如下 final K key; 看到next了嗎?next就是為了哈希沖突而存在的。比如通過(guò)哈希運(yùn)算,一個(gè)新元素應(yīng)該在數(shù)組的第10個(gè)位置,但是第10個(gè)位置已經(jīng)有Entry,那么好吧,將新加的元素也放到第10個(gè)位置,將第10個(gè)位置的原有Entry賦值給當(dāng)前新加的Entry的next屬性。數(shù)組存儲(chǔ)的鏈表,鏈表是為了解決哈希沖突的,這一點(diǎn)要注意。 幾個(gè)關(guān)鍵的屬性 存儲(chǔ)數(shù)據(jù)的數(shù)組 transient Entry[] table; 這個(gè)上面已經(jīng)講到了 static final int DEFAULT_INITIAL_CAPACITY = 16; 最大容量 static final int MAXIMUM_CAPACITY = 1 << 30; 默認(rèn)加載因子,加載因子是一個(gè)比例,當(dāng)HashMap的數(shù)據(jù)大小>=容量*加載因子時(shí),HashMap會(huì)將容量擴(kuò)容 static final float DEFAULT_LOAD_FACTOR = 0.75f; 當(dāng)實(shí)際數(shù)據(jù)大小超過(guò)threshold時(shí),HashMap會(huì)將容量擴(kuò)容,threshold=容量*加載因子 int threshold; 加載因子 final float loadFactor; HashMap的初始過(guò)程 構(gòu)造函數(shù)1 public HashMap(int initialCapacity, float loadFactor) { 重點(diǎn)注意這里 while (capacity < initialCapacity) capacity才是初始容量,而不是initialCapacity,這個(gè)要特別注意,如果執(zhí)行new HashMap(9,0.75);那么HashMap的初始容量是16,而不是9,想想為什么吧。 構(gòu)造函數(shù)2 public HashMap(int initialCapacity) { 構(gòu)造函數(shù)3,全部都是默認(rèn)值 public HashMap() { 構(gòu)造函數(shù)4 public HashMap(Map<? extends K, ? extends V> m) { 如何哈希 HashMap并不是直接將對(duì)象的hashcode作為哈希值的,而是要把key的hashcode作一些運(yùn)算以得到最終的哈希值,并且得到的哈希值也不是在數(shù)組中的位置哦,無(wú)論是get還是put還是別的方法,計(jì)算哈希值都是這一句: int hash = hash(key.hashCode()); hash函數(shù)如下: static int hash(int h) { useNewHash聲明如下: private static final boolean useNewHash; 這說(shuō)明useNewHash其實(shí)一直為false且不可改變的,hash函數(shù)里對(duì)useNewHash的判斷真是多余的。 private static int oldHash(int h) { private static int newHash(int h) { 其實(shí)HashMap的哈希函數(shù)會(huì)一直都是oldHash。
如果確定數(shù)據(jù)的位置 看下面兩行 int hash = hash(k.hashCode()); 第一行,上面講過(guò)了,是得到哈希值,第二行,則是根據(jù)哈希指計(jì)算元素在數(shù)組中的位置了,位置的計(jì)算是將哈希值和數(shù)組長(zhǎng)度按位與運(yùn)算。 static int indexFor(int h, int length) {
put方法到底作了什么? public V put(K key, V value) { 如果key為NULL,則是單獨(dú)處理的,看看putForNullKey方法: private V putForNullKey(V value) { NULL_KEY的聲明:static final Object NULL_KEY = new Object(); 這一段代碼是處理哈希沖突的,就是說(shuō),在數(shù)組某個(gè)位置的對(duì)象可能并不是唯一的,它是一個(gè)鏈表結(jié)構(gòu),根據(jù)哈希值找到鏈表后,還要對(duì)鏈表遍歷,找出key相等的對(duì)象,替換它,并且返回舊的值。 for (Entry<K,V> e = table[i]; e != null; e = e.next) { 如果遍歷完了該位置的鏈表都沒(méi)有找到有key相等的,那么將當(dāng)前對(duì)象增加到鏈表里面去 modCount++; 且看看addEntry方法 void addEntry(int hash, K key, V value, int bucketIndex) { table[bucketIndex] = new Entry<K,V>(hash, key, value, e);新建一個(gè)Entry對(duì)象,并放在當(dāng)前位置的Entry鏈表的頭部,看看下面的 Entry構(gòu)造函數(shù)就知道了,注意紅色部分。 Entry(int h, K k, V v, Entry<K,V> n) {
如何擴(kuò)容? 當(dāng)put一個(gè)元素時(shí),如果達(dá)到了容量限制,HashMap就會(huì)擴(kuò)容,新的容量永遠(yuǎn)是原來(lái)的2倍。 上面的put方法里有這樣的一段: if (size++ >= threshold) 這是擴(kuò)容判斷,要注意,并不是數(shù)據(jù)尺寸達(dá)到HashMap的最大容量時(shí)才擴(kuò)容,而是達(dá)到 threshold指定的值時(shí)就開(kāi)始擴(kuò)容,threshold=最大容量*加載因子。 看看resize方法 void resize(int newCapacity) { 重點(diǎn)看看紅色部分的 transfer方法 void transfer(Entry[] newTable) { tranfer方法將所有的元素重新哈希,因?yàn)樾碌娜萘孔兇?,所以每個(gè)元素的哈希值和位置都是不一樣的。 正確的使用HashMap 1:不要在并發(fā)場(chǎng)景中使用HashMap HashMap是線程不安全的,如果被多個(gè)線程共享的操作,將會(huì)引發(fā)不可預(yù)知的問(wèn)題,據(jù)sun的說(shuō)法,在擴(kuò)容時(shí),會(huì)引起鏈表的閉環(huán),在get元素時(shí),就會(huì)無(wú)限循環(huán),后果是cpu100%。 看看get方法的紅色部分 public V get(Object key) { 2:如果數(shù)據(jù)大小是固定的,那么最好給HashMap設(shè)定一個(gè)合理的容量值 根據(jù)上面的分析,HashMap的初始默認(rèn)容量是16,默認(rèn)加載因子是0.75,也就是說(shuō),如果采用HashMap的默認(rèn)構(gòu)造函數(shù),當(dāng)增加數(shù)據(jù)時(shí),數(shù)據(jù)實(shí)際容量超過(guò)10*0.75=12時(shí),HashMap就擴(kuò)容,擴(kuò)容帶來(lái)一系列的運(yùn)算,新建一個(gè)是原來(lái)容量2倍的數(shù)組,對(duì)原有元素全部重新哈希,如果你的數(shù)據(jù)有幾千幾萬(wàn)個(gè),而用默認(rèn)的HashMap構(gòu)造函數(shù),那結(jié)果是非常悲劇的,因?yàn)镠ashMap不斷擴(kuò)容,不斷哈希,在使用HashMap的場(chǎng)景里,不會(huì)是多個(gè)線程共享一個(gè)HashMap,除非對(duì)HashMap包裝并同步,由此產(chǎn)生的內(nèi)存開(kāi)銷和cpu開(kāi)銷在某些情況下可能是致命的。 |
|