Hashmap非線程安全關(guān)于hash值沖突處理
前言
總是覺得對HashMap很熟悉,但最近連續(xù)被問到幾個關(guān)于它的問題,才發(fā)現(xiàn)它其實(shí)并不簡單。這里對關(guān)于它的一些問題做個總結(jié),也希望能夠大家一個參考。
hash值沖突該怎么處理
都知道它是基于hash值,可以進(jìn)行常量時間消化的存儲結(jié)構(gòu)。廣泛用于各種情況下的高效key-value存儲。這里有幾個問題。首先,如果出現(xiàn)hash值沖突該怎么處理?相信很多人都不用思考就能說出,通過鏈表來解決沖突。就是將hash值相同值存儲到一個掛載在該位置的鏈表里面去。但是這就又引出了新的問題,給一個key,它的hash值有沖突的情況下,它是如何在鏈表上取到你所期望的value的?這個就要去看hashmap的存儲結(jié)構(gòu)了。每一個key-value會被封裝到一個entity中,map中存儲的其實(shí)是這個entity。這樣既存儲了value,又存儲了key。所有在鏈表上取值只需要比較key是否equal。這里又出現(xiàn)了一個問題,為什么建議重寫key的equal和hash方法。重寫hash方法能夠保證不同對象用于不同的hash值,從而減少沖突,重寫equal則可以在出現(xiàn)沖突的情況下,保證不出現(xiàn)錯誤覆蓋的情況。
hashmap的put方法
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node<K,V>[] tab; Node<K,V> p; int n, i; if ((tab = table) == null || (n = tab.length) == 0)//沒有初始化,則要初始化,初始化調(diào)用的也是resize()方法 n = (tab = resize()).length; if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null);//這個位置沒有值,直接插入,重寫hash方法的作用體現(xiàn)在這里 else {//出現(xiàn)了沖突 Node<K,V> e; K k; if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))//找到了如該key equal的value e = p; else if (p instanceof TreeNode)//事樹節(jié)點(diǎn),插入到樹里。jdk8沖突節(jié)點(diǎn)數(shù)目達(dá)到一定值后,使用樹結(jié)構(gòu)存儲 e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); else { for (int binCount = 0; ; ++binCount) {//一直找到鏈表的結(jié)尾,將k-v插入 if ((e = p.next) == null) { p.next = newNode(hash, key, value, null); if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st treeifyBin(tab, hash);//是否需要改成樹存儲 break; } if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; p = e; } } if (e != null) { // existing mapping for key V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); return oldValue; } } ++modCount; if (++size > threshold) resize(); afterNodeInsertion(evict); return null; }
上面的代碼充分表明了key重寫equal的作用。HashMap以key的value來獲取值,所以一定要確保同一個key的hashcode在任何時候都不能改變。這也是為什么建議key是不可變對象,如string對象。如果key是對象,運(yùn)行過程中key的hashcode因?yàn)閮?nèi)部值的改變而發(fā)生了改變,那么map中的value將永遠(yuǎn)丟失。
非線程安全體現(xiàn)
以上是關(guān)于kv的討論,接下來是關(guān)于HashMap的另外的一個常見話題——線程安全問題。多數(shù)人都知道它是非線程安全的。但是如果問你它非線程安全體現(xiàn)在哪里,恐怕會難住一批人。
首先容易想到的是多線程插入時出現(xiàn)了沖突的情況,多個線程同時插入,但是這其中有些值又出現(xiàn)了沖突。插入時大家都看到這個位置沒有值,于是都進(jìn)行插入,這樣肯定會出現(xiàn)值覆蓋,對外的表現(xiàn)就是值丟失。如果開始插入時,這個位置已經(jīng)有了值,那么在插入鏈表過程中還是會出現(xiàn)值覆蓋。
另外就是同時擴(kuò)容問題。因?yàn)镠ashMap會在空間不足時自動擴(kuò)容,大小變成之前的兩倍。同時復(fù)制之前的值到新的數(shù)組中。沖突鏈也會進(jìn)行復(fù)制。如果多個線程插入后同時看到容量需要調(diào)整,就都會調(diào)用resize方法。那么底層到達(dá)會出現(xiàn)什么問題就難以預(yù)測了。
所以這也是HashMap非線程安全的第二點(diǎn)體現(xiàn)。當(dāng)然同時讀寫一個值也可能會存在數(shù)據(jù)跟期望不一致的情況。這也是非線程安全的表現(xiàn)。
以上就是HashMap的一些相關(guān)問題。個人體會還是需要注重細(xì)節(jié),自己看源碼,才會有更深入的體會。
更多關(guān)于Hashmap非線程安全的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
使用jpa的實(shí)體對象轉(zhuǎn)json符串時懶加載的問題及解決
這篇文章主要介紹了使用jpa的實(shí)體對象轉(zhuǎn)json符串時懶加載的問題及解決方案,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-02-02SpringBoot配置使用H2數(shù)據(jù)庫的簡單教程
H2是一個Java編寫的關(guān)系型數(shù)據(jù)庫,它可以被嵌入Java應(yīng)用程序中使用,或者作為一個單獨(dú)的數(shù)據(jù)庫服務(wù)器運(yùn)行。本文將介紹SpringBoot如何配置使用H2數(shù)據(jù)庫2021-05-05Spring Boot項目中實(shí)現(xiàn)文件上傳功能的示例
這篇文章主要介紹了Spring Boot項目中實(shí)現(xiàn)文件上傳功能,本文通過實(shí)例代碼給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-12-12Java實(shí)現(xiàn)經(jīng)典游戲2048的示例代碼
2014年Gabriele Cirulli利用周末的時間寫2048這個游戲的程序。本文將用java語言實(shí)現(xiàn)這一經(jīng)典游戲,并采用了swing技術(shù)進(jìn)行了界面化處理,需要的可以參考一下2022-02-02Java編程之jdk1.4,jdk1.5和jdk1.6的區(qū)別分析(經(jīng)典)
這篇文章主要介紹了Java編程之jdk1.4,jdk1.5和jdk1.6的區(qū)別分析,結(jié)合實(shí)例形式較為詳細(xì)的分析說明了jdk1.4,jdk1.5和jdk1.6版本的使用區(qū)別,需要的朋友可以參考下2015-12-12SpringBoot單機(jī)限流的實(shí)現(xiàn)
在系統(tǒng)運(yùn)維中, 有時候?yàn)榱吮苊庥脩舻膼阂馑⒔涌? 會加入一定規(guī)則的限流,本文主要介紹了SpringBoot單機(jī)限流的實(shí)現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2023-08-08Spring?RestTemplate如何利用攔截器打印請求參數(shù)和返回狀態(tài)
這篇文章主要介紹了Spring?RestTemplate如何利用攔截器打印請求參數(shù)和返回狀態(tài)問題,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2023-07-07