面試必問(wèn):hashmap為什么會(huì)出現(xiàn)死循環(huán)?
jdk1.7 hashmap的循環(huán)依賴問(wèn)題是面試經(jīng)常被問(wèn)到的問(wèn)題,如何回答不好,可能會(huì)被扣分。今天我就帶大家一下梳理一下,這個(gè)問(wèn)題是如何產(chǎn)生的,以及如何解決這個(gè)問(wèn)題。
一、hashmap的數(shù)據(jù)結(jié)構(gòu)
先一起看看jdk1.7 hashmap的數(shù)據(jù)結(jié)構(gòu)
數(shù)組 + 鏈表
hashmap會(huì)給每個(gè)元素的key生成一個(gè)hash值,然后根據(jù)這個(gè)hash值計(jì)算一個(gè)在數(shù)組中的位置i。i不同的元素放在數(shù)組的不同位置,i相同的元素放在鏈表上,最新的數(shù)據(jù)放在鏈表的頭部。
往hashmap中保存元素會(huì)調(diào)用put方法,獲取元素會(huì)調(diào)用get方法。接下來(lái),我們重點(diǎn)看看put方法。
二、put方法
重點(diǎn)看看put方法
- public V put(K key, V value) {
- if (table == EMPTY_TABLE) {
- inflateTable(threshold); } if (key == null)
- return putForNullKey(value);
- //根據(jù)key獲取hash
- int hash = hash(key); //計(jì)算在數(shù)組中的下表
- int i = indexFor(hash, table.length); //變量集合查詢相同key的數(shù)據(jù),如果已經(jīng)存在則更新數(shù)據(jù)
- 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); //返回已有數(shù)據(jù)
- return oldValue;
- } } modCount++; //如果不存在相同key的元素,則添加新元素
- addEntry(hash, key, value, i); return null;
- }
再看看addEntry方法
- void addEntry(int hash, K key, V value, int bucketIndex) {
- // 當(dāng)數(shù)組的size >= 擴(kuò)容閾值,觸發(fā)擴(kuò)容,size大小會(huì)在createEnty和removeEntry的時(shí)候改變 if ((size >= threshold) && (null != table[bucketIndex])) {
- // 擴(kuò)容到2倍大小,后邊會(huì)跟進(jìn)這個(gè)方法 resize(2 * table.length); // 擴(kuò)容后重新計(jì)算hash和index
- hash = (null != key) ? hash(key) : 0;
- bucketIndex = indexFor(hash, table.length);
- } // 創(chuàng)建一個(gè)新的鏈表節(jié)點(diǎn),點(diǎn)進(jìn)去可以了解到是將新節(jié)點(diǎn)添加到了鏈表的頭部 createEntry(hash, key, value, bucketIndex);
- }
看看resize是如何擴(kuò)容的
- void resize(int newCapacity) {
- Entry[] oldTable = table; int oldCapacity = oldTable.length;
- if (oldCapacity == MAXIMUM_CAPACITY) {
- threshold = Integer.MAX_VALUE; return;
- } // 創(chuàng)建2倍大小的新數(shù)組
- Entry[] newTable = new Entry[newCapacity];
- // 將舊數(shù)組的鏈表轉(zhuǎn)移到新數(shù)組,就是這個(gè)方法導(dǎo)致的hashMap不安全,等下我們進(jìn)去看一眼
- transfer(newTable, initHashSeedAsNeeded(newCapacity));
- table = newTable;
- // 重新計(jì)算擴(kuò)容閾值(容量*加載因子)
- threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
- }
出問(wèn)題的就是這個(gè)transfer方法
- void transfer(Entry[] newTable, boolean rehash) {
- int newCapacity = newTable.length; // 遍歷舊數(shù)組
- for (Entry<K,V> e : table) {
- // 遍歷鏈表
- while(null != e) {
- //獲取下一個(gè)元素,記錄到一個(gè)臨時(shí)變量,以便后面使用
- Entry<K,V> next = e.next;
- if (rehash) {
- e.hash = null == e.key ? 0 : hash(e.key);
- } // 計(jì)算節(jié)點(diǎn)在新數(shù)組中的下標(biāo)
- int i = indexFor(e.hash, newCapacity);
- // 將舊節(jié)點(diǎn)插入到新節(jié)點(diǎn)的頭部
- e.next = newTable[i];
- //這行才是真正把數(shù)據(jù)插入新數(shù)組中,前面那行代碼只是設(shè)置當(dāng)前節(jié)點(diǎn)的next
- //這兩行代碼決定了倒序插入
- //比如:以前同一個(gè)位置上是:3,7,后面可能變成了:7、3
- newTable[i] = e;
- //將下一個(gè)元素賦值給當(dāng)前元素,以便遍歷下一個(gè)元素
- e = next;
- }
- }
- }
我來(lái)給大家分析一下,為什么這幾個(gè)代碼是頭插法,網(wǎng)上很多技術(shù)文章都沒(méi)有說(shuō)清楚。
三、頭插法
我們把目光聚焦到這幾行代碼:
- //獲取下一個(gè)元素,記錄到一個(gè)臨時(shí)變量,以便后面使用
- Entry<K,V> next = e.next;
- // 計(jì)算節(jié)點(diǎn)在新數(shù)組中的下標(biāo) int i = indexFor(e.hash, newCapacity); // 將舊節(jié)點(diǎn)插入到新節(jié)點(diǎn)的頭部 e.next = newTable[i];
- //這行才是真正把數(shù)據(jù)插入新數(shù)組中,前面那行代碼只是設(shè)置當(dāng)前節(jié)點(diǎn)的next
- newTable[i] = e; //將下一個(gè)元素賦值給當(dāng)前元素,以便遍歷下一個(gè)元素 e = next;
假設(shè)剛開(kāi)始hashMap有這些數(shù)據(jù)
調(diào)用put方法需要進(jìn)行一次擴(kuò)容,剛開(kāi)始會(huì)創(chuàng)建一個(gè)空的數(shù)組,大小是以前的2倍,如圖所示:
開(kāi)始第一輪循環(huán):
- //next= 7 e = 3 e.next = 7
- Entry<K,V> next = e.next;
- // i=3
- int i = indexFor(e.hash, newCapacity);//e.next = null ,剛初始化時(shí)新數(shù)組的元素為null
- e.next = newTable[i];
- //給新數(shù)組i位置 賦值 3
- newTable[i] = e;// e = 7
- e = next;
執(zhí)行完之后,第一輪循環(huán)之后數(shù)據(jù)變成這樣的
再接著開(kāi)始第二輪循環(huán):
- //next= 5 e = 7 e.next = 5
- Entry<K,V> next = e.next;
- // i=3
- int i = indexFor(e.hash, newCapacity);//e.next = 3 ,此時(shí)相同位置上已經(jīng)有key=3的值了,將該值賦值給當(dāng)前元素的next
- e.next = newTable[i];
- //給新數(shù)組i位置 賦值 7
- newTable[i] = e;// e = 5
- e = next;
上面會(huì)構(gòu)成一個(gè)新鏈表,連接的順序正好反過(guò)來(lái)了。
由于第二次循環(huán)時(shí),節(jié)點(diǎn)key=7的元素插到相同位置上已有元素key=3的前面,所以說(shuō)是采用的頭插法。
四、死循環(huán)的產(chǎn)生
接下來(lái)重點(diǎn)看看死循環(huán)是如何產(chǎn)生的?
假設(shè)數(shù)據(jù)跟元素?cái)?shù)據(jù)一致,有兩個(gè)線程:線程1 和 線程2,同時(shí)執(zhí)行put方法,最后同時(shí)調(diào)用transfer方法。
線程1 先執(zhí)行,到 Entry next = e.next; 這一行,被掛起了。
- //next= 7 e = 3 e.next = 7
- Entry<K,V> next = e.next;
- int i = indexFor(e.hash, newCapacity);e.next = newTable[i];
- newTable[i] = e;e = next;
此時(shí)線程1 創(chuàng)建的數(shù)組會(huì)創(chuàng)建一個(gè)空數(shù)組
接下來(lái),線程2開(kāi)始執(zhí)行,由于線程2運(yùn)氣比較好,沒(méi)有被中斷過(guò),執(zhí)行完畢了。
過(guò)一會(huì)兒,線程1被恢復(fù)了,重新執(zhí)行代碼。
- //next= 7 e = 3 e.next = 7
- Entry<K,V> next = e.next;
- // i = 3
- int i = indexFor(e.hash, newCapacity);// e.next = null,剛初始化時(shí)新數(shù)組的元素為null
- e.next = newTable[i];
- // 給新數(shù)組i位置 賦值 3
- newTable[i] = e;// e = 7
- e = next;
這時(shí)候線程1的數(shù)組會(huì)變成這樣的
再執(zhí)行第二輪循環(huán),此時(shí)的e=7
- //next= 3 e = 7 e.next = 3
- Entry<K,V> next = e.next;
- // i = 3
- int i = indexFor(e.hash, newCapacity);// e.next = 3,此時(shí)相同位置上已經(jīng)有key=3的值了,將該值賦值給當(dāng)前元素的next
- e.next = newTable[i];
- // 給新數(shù)組i位置 賦值 7
- newTable[i] = e;// e = 3
- e = next;
這里特別要說(shuō)明的是 此時(shí)e=7,而e.next為什么是3呢?
因?yàn)閔ashMap的數(shù)據(jù)是公共的,還記得線程2中的生成的數(shù)據(jù)嗎?
此時(shí)e=7,那么e.next肯定是3。
經(jīng)過(guò)上面第二輪循環(huán)之后,線程1得到的數(shù)據(jù)如下:
此時(shí)由于循環(huán)判斷還沒(méi)有退出,判斷條件是: while(null != e),所以要開(kāi)始第三輪循環(huán):
- //next= null e = 3 e.next = null
- Entry<K,V> next = e.next;
- // i = 3
- int i = indexFor(e.hash, newCapacity);// e.next = 7,關(guān)鍵的一步,由于第二次循環(huán)是 key:7 .next = key:3,現(xiàn)在key:3.next = key:7
- e.next = newTable[i];
- // 給新數(shù)組i位置 賦值 3
- newTable[i] = e;// e = nulle = next;
由于e=null,此時(shí)會(huì)退出循環(huán),最終線程1的數(shù)據(jù)會(huì)是這種結(jié)構(gòu):
key:3 和 key:7又恢復(fù)了剛開(kāi)始的順序,但是他們的next會(huì)相互引用,構(gòu)成環(huán)形引用。
注意,此時(shí)調(diào)用hashmap的get方法獲取數(shù)據(jù)時(shí),如果只是獲取循環(huán)鏈上key:3 和 key:7的數(shù)據(jù),是不會(huì)有問(wèn)題的,因?yàn)榭梢哉业健>团芦@取循環(huán)鏈上沒(méi)有的數(shù)據(jù),比如:key:11,key:15等,會(huì)進(jìn)入無(wú)限循環(huán)中導(dǎo)致CPU使用率飆升。
五、如何避免死循環(huán)
為了解決這個(gè)問(wèn)題,jdk1.8把擴(kuò)容是復(fù)制元素到新數(shù)組由 頭插法 改成了 尾插法 。此外,引入了紅黑樹(shù),提升遍歷節(jié)點(diǎn)的效率。在這里我就不過(guò)多介紹了,如果有興趣的朋友,可以關(guān)注我的公眾號(hào),后面會(huì)給大家詳細(xì)分析jdk1.8的實(shí)現(xiàn),以及 jdk1.7、jdk1.8 hashmap的區(qū)別。
此外,HashMap是非線程安全的,要避免在多線程的環(huán)境中使用HashMap,而應(yīng)該改成使用ConcurrentHashMap。
所以總結(jié)一下要避免發(fā)生死循環(huán)的問(wèn)題的方法:
- 改成ConcurrentHashMap
PS. 即使JDK升級(jí)到1.8任然有死循環(huán)的問(wèn)題。