Java中的HashMap和ConcurrentHashMap區(qū)別和適用場(chǎng)景
HashMap和ConcurrentHashMap在對(duì)null值的處理、線程安全性、性能等方面存在顯著的區(qū)別。HashMap允許鍵和值為null,適用于單線程環(huán)境下的數(shù)據(jù)存儲(chǔ)和查詢場(chǎng)景,具有較高的性能和簡(jiǎn)單的使用方式;而ConcurrentHashMap不允許鍵和值為null,適用于多線程環(huán)境下的數(shù)據(jù)存儲(chǔ)和查詢場(chǎng)景,具有線程安全性和較高的并發(fā)性能。
1. 對(duì)null值的處理
1.1 HashMap對(duì)null值的處理
HashMap允許鍵(key)和值(value)都為null。這種設(shè)計(jì)使得HashMap在某些場(chǎng)景下更加靈活。例如,在處理一些可能存在空值的數(shù)據(jù)源時(shí),可以直接將數(shù)據(jù)存儲(chǔ)到HashMap中,而不需要額外的非空判斷或轉(zhuǎn)換。以下是一個(gè)簡(jiǎn)單的示例代碼:
HashMap<String, Object> map = new HashMap<>(); map.put(null, null); // 正常執(zhí)行,key 和 value 都為 null if (map.containsKey(null)) { System.out.println("存在 null"); } else { System.out.println("不存在 null"); }
執(zhí)行上述代碼,控制臺(tái)會(huì)輸出“存在 null”,表明HashMap成功地將null作為鍵和值存儲(chǔ),并且可以通過(guò)containsKey(null)方法準(zhǔn)確地判斷出null鍵的存在。
1.2 ConcurrentHashMap對(duì)null值的處理
與HashMap不同,ConcurrentHashMap不允許鍵(key)和值(value)為null。如果嘗試將null作為鍵或值插入到ConcurrentHashMap中,會(huì)拋出NullPointerException異常。以下是兩個(gè)示例代碼:
ConcurrentHashMap<String, String> concurrentHashMap = new ConcurrentHashMap<>(); concurrentHashMap.put(null, "javacn.site"); // 拋出 NullPointerException
String key = "www.Javacn.site"; ConcurrentHashMap<String, String> concurrentHashMap = new ConcurrentHashMap<>(); concurrentHashMap.put(key, null); // 拋出 NullPointerException
在上述兩個(gè)代碼片段中,無(wú)論是將null作為鍵還是值插入到ConcurrentHashMap中,都會(huì)導(dǎo)致程序異常終止,并拋出NullPointerException異常。
1.3 為什么ConcurrentHashMap不能插入null?
要理解ConcurrentHashMap為什么不能插入null值,我們需要從其源碼層面進(jìn)行分析。以下是ConcurrentHashMap添加元素時(shí)的部分核心源碼:
// 添加 key 和 value public V put(K key, V value) { return putVal(key, value, false); } final V putVal(K key, V value, boolean onlyIfAbsent) { // 如果 key 或 value 為 null 的話直接拋出空指針異常 if (key == null || value == null) throw new NullPointerException(); int hash = spread(key.hashCode()); int binCount = 0; // 忽略其他代碼...... }
從上述源碼可以看出,在putVal方法的第一行,ConcurrentHashMap就對(duì)key和value進(jìn)行了null檢查,如果發(fā)現(xiàn)它們中的任何一個(gè)為null,就會(huì)直接拋出NullPointerException異常。這種設(shè)計(jì)是ConcurrentHashMap的一個(gè)明確的約束條件,旨在避免因null值引起的潛在問(wèn)題。
1.4 更深層次的原因
那么,為什么ConcurrentHashMap的實(shí)現(xiàn)源碼中要明確禁止key或value為null呢?這要從ConcurrentHashMap的使用場(chǎng)景和并發(fā)環(huán)境下的特殊性來(lái)分析。
1.4.1 二義性問(wèn)題
在并發(fā)環(huán)境下,如果允許ConcurrentHashMap的key或value為null,就會(huì)存在經(jīng)典的“二義性問(wèn)題”。二義性問(wèn)題指的是代碼或表達(dá)式存在多種理解或解釋?zhuān)瑢?dǎo)致程序的含義不明確或模糊。對(duì)于ConcurrentHashMap來(lái)說(shuō),null值的二義性主要體現(xiàn)在以下兩個(gè)方面:
- null作為具體值:當(dāng)我們將null作為值存儲(chǔ)到ConcurrentHashMap中時(shí),null表示的就是一個(gè)具體的“null”值狀態(tài)。例如,某個(gè)業(yè)務(wù)場(chǎng)景中,某個(gè)屬性的值可能就是null,表示該屬性沒(méi)有具體的值或者不適用。
- null表示“沒(méi)有”:在某些情況下,null還可以表示“沒(méi)有”的意思,即某個(gè)鍵在ConcurrentHashMap中根本沒(méi)有對(duì)應(yīng)的值。例如,當(dāng)我們查詢ConcurrentHashMap時(shí),如果返回null,可能是因?yàn)樵撴I從未被插入過(guò),或者該鍵對(duì)應(yīng)的值確實(shí)就是null。
如果ConcurrentHashMap允許插入null值,那么在并發(fā)環(huán)境下,當(dāng)我們查詢某個(gè)鍵時(shí),得到的null值就無(wú)法明確區(qū)分是上述哪種情況,從而導(dǎo)致二義性問(wèn)題。
1.4.2 HashMap的可證偽性
相比之下,HashMap允許插入null值,但它不怕二義性問(wèn)題的原因在于,HashMap是為單線程環(huán)境設(shè)計(jì)的。在單線程環(huán)境下,二義性問(wèn)題是可被證明真?zhèn)蔚?。例如,?dāng)我們給HashMap的key設(shè)置為null時(shí),可以通過(guò)hashMap.containsKey(key)的方法來(lái)區(qū)分這個(gè)null值到底是存入的null,還是壓根不存在的null。因?yàn)閱尉€程環(huán)境下,數(shù)據(jù)的修改和查詢是順序執(zhí)行的,不會(huì)受到其他線程的干擾,所以二義性問(wèn)題可以被明確地解決。
1.4.3 ConcurrentHashMap的不可證偽性
而ConcurrentHashMap是為多線程環(huán)境設(shè)計(jì)的,多線程下的二義性問(wèn)題是不能被證明真?zhèn)蔚?。因?yàn)樵谧C明二義性問(wèn)題的同時(shí),可能會(huì)有其他線程影響執(zhí)行結(jié)果,導(dǎo)致結(jié)果不準(zhǔn)確。例如,當(dāng)ConcurrentHashMap未設(shè)置key為null時(shí),可能會(huì)出現(xiàn)以下場(chǎng)景:一個(gè)線程A調(diào)用了concurrentHashMap.containsKey(key),期望返回的結(jié)果是false,但在調(diào)用該方法之后,未返回結(jié)果之前,線程B又調(diào)用了concurrentHashMap.put(key, null)存入了null值,那么線程A最終返回的結(jié)果就是true了。這個(gè)結(jié)果與線程A之前預(yù)想的false完全不一樣,這就是不能被證偽的二義性問(wèn)題。
為了避免這種復(fù)雜的二義性問(wèn)題,ConcurrentHashMap選擇在源碼中明確禁止null值作為key或value,從而簡(jiǎn)化了并發(fā)環(huán)境下的數(shù)據(jù)管理邏輯,確保了數(shù)據(jù)的一致性和準(zhǔn)確性。
2. 線程安全性
2.1 HashMap的線程安全性
HashMap是非線程安全的,這意味著在多線程環(huán)境下使用HashMap時(shí),可能會(huì)遇到各種并發(fā)問(wèn)題,如數(shù)據(jù)丟失、數(shù)據(jù)重復(fù)、死鎖等。具體來(lái)說(shuō),HashMap的線程不安全性主要體現(xiàn)在以下幾個(gè)方面:
- 數(shù)據(jù)丟失:當(dāng)多個(gè)線程同時(shí)對(duì)HashMap進(jìn)行插入操作時(shí),可能會(huì)出現(xiàn)數(shù)據(jù)丟失的情況。例如,兩個(gè)線程同時(shí)插入相同的鍵但不同的值,最終可能會(huì)導(dǎo)致只有一個(gè)值被成功插入,而另一個(gè)值被覆蓋。
- 數(shù)據(jù)重復(fù):在某些情況下,多個(gè)線程可能會(huì)插入重復(fù)的數(shù)據(jù)。例如,當(dāng)多個(gè)線程同時(shí)檢查某個(gè)鍵是否存在,并且發(fā)現(xiàn)該鍵不存在時(shí),它們可能會(huì)同時(shí)插入相同的鍵值對(duì),導(dǎo)致數(shù)據(jù)重復(fù)。
- 死鎖:在某些復(fù)雜的操作中,如擴(kuò)容時(shí),HashMap可能會(huì)出現(xiàn)死鎖問(wèn)題。擴(kuò)容操作需要重新計(jì)算所有鍵的哈希值,并將它們重新分配到新的桶數(shù)組中。如果多個(gè)線程同時(shí)進(jìn)行擴(kuò)容操作,可能會(huì)導(dǎo)致死鎖,從而影響程序的正常運(yùn)行。
2.2 ConcurrentHashMap的線程安全性
與HashMap不同,ConcurrentHashMap是線程安全的,它通過(guò)多種機(jī)制來(lái)保證在多線程環(huán)境下的安全性。以下是ConcurrentHashMap實(shí)現(xiàn)線程安全性的主要機(jī)制:
- 分段鎖(Segment):在早期的版本中,ConcurrentHashMap使用分段鎖來(lái)實(shí)現(xiàn)線程安全。它將整個(gè)哈希表分為多個(gè)段(Segment),每個(gè)段相當(dāng)于一個(gè)小的哈希表,并且每個(gè)段都有自己的鎖。當(dāng)對(duì)ConcurrentHashMap進(jìn)行操作時(shí),只需要鎖定相關(guān)的段,而不需要鎖定整個(gè)哈希表。這樣可以顯著提高并發(fā)性能,因?yàn)槎鄠€(gè)線程可以同時(shí)對(duì)不同的段進(jìn)行操作,而不會(huì)相互干擾。
- CAS操作:ConcurrentHashMap使用了Compare-And-Swap(CAS)操作來(lái)保證某些操作的原子性。CAS是一種無(wú)鎖的非阻塞算法,它通過(guò)比較內(nèi)存中的值與預(yù)期值是否相等,如果相等,則將內(nèi)存中的值更新為新值。例如,在插入鍵值對(duì)時(shí),ConcurrentHashMap會(huì)使用CAS操作來(lái)更新節(jié)點(diǎn)的引用,從而保證插入操作的原子性。
- 鎖分離:ConcurrentHashMap將讀操作和寫(xiě)操作分開(kāi)處理,讀操作不需要加鎖,而寫(xiě)操作則需要加鎖。這種鎖分離機(jī)制可以提高并發(fā)性能,因?yàn)槎鄠€(gè)線程可以同時(shí)進(jìn)行讀操作,而不會(huì)受到寫(xiě)操作的影響。只有當(dāng)進(jìn)行寫(xiě)操作時(shí),才會(huì)對(duì)相關(guān)部分加鎖,從而保證數(shù)據(jù)的一致性和安全性。
2.3 線程安全性對(duì)性能的影響
線程安全性對(duì)性能有著直接的影響。對(duì)于HashMap來(lái)說(shuō),由于其非線程安全的特性,在單線程環(huán)境下可以提供較高的性能,因?yàn)椴恍枰M(jìn)行額外的鎖操作和同步處理。然而,在多線程環(huán)境下,HashMap的性能會(huì)受到嚴(yán)重影響,因?yàn)樾枰~外的同步機(jī)制來(lái)保證線程安全,如使用Collections.synchronizedMap方法對(duì)HashMap進(jìn)行包裝,或者在使用HashMap時(shí)手動(dòng)進(jìn)行同步處理。
相比之下,ConcurrentHashMap由于其線程安全的特性,在多線程環(huán)境下可以提供較高的性能。它通過(guò)分段鎖、CAS操作和鎖分離等機(jī)制,減少了鎖的粒度和鎖的爭(zhēng)用,從而提高了并發(fā)性能。在多線程環(huán)境下,多個(gè)線程可以同時(shí)對(duì)ConcurrentHashMap進(jìn)行讀寫(xiě)操作,而不會(huì)出現(xiàn)嚴(yán)重的性能瓶頸。當(dāng)然,在單線程環(huán)境下,ConcurrentHashMap的性能可能會(huì)略低于HashMap,因?yàn)槠鋬?nèi)部的線程安全機(jī)制會(huì)帶來(lái)一定的開(kāi)銷(xiāo)。
3. 性能比較
3.1 時(shí)間復(fù)雜度
從時(shí)間復(fù)雜度的角度來(lái)看,HashMap和ConcurrentHashMap在大多數(shù)操作上的時(shí)間復(fù)雜度都是O(1),即常數(shù)時(shí)間復(fù)雜度。這是因?yàn)樗鼈兌际腔诠1韺?shí)現(xiàn)的,通過(guò)計(jì)算鍵的哈希值來(lái)快速定位對(duì)應(yīng)的桶(bucket),從而實(shí)現(xiàn)快速的插入、刪除和查找操作。
然而,在某些情況下,時(shí)間復(fù)雜度可能會(huì)退化到O(n),即線性時(shí)間復(fù)雜度。例如,當(dāng)哈希表中出現(xiàn)大量哈希沖突時(shí),即多個(gè)鍵的哈希值相同或相近,導(dǎo)致它們被分配到同一個(gè)桶中,此時(shí)需要遍歷桶中的鏈表或紅黑樹(shù)來(lái)找到對(duì)應(yīng)的鍵值對(duì),時(shí)間復(fù)雜度會(huì)退化到O(n)。不過(guò),這種情況在正常情況下是較少出現(xiàn)的,因?yàn)榱己玫墓:瘮?shù)和合理的擴(kuò)容機(jī)制可以有效地減少哈希沖突的發(fā)生。
3.2 并發(fā)性能
在并發(fā)性能方面,ConcurrentHashMap顯然優(yōu)于HashMap。如前所述,ConcurrentHashMap通過(guò)分段鎖、CAS操作和鎖分離等機(jī)制,減少了鎖的粒度和鎖的爭(zhēng)用,從而提高了并發(fā)性能。在多線程環(huán)境下,多個(gè)線程可以同時(shí)對(duì)ConcurrentHashMap進(jìn)行讀寫(xiě)操作,而不會(huì)出現(xiàn)嚴(yán)重的性能瓶頸。
相比之下,HashMap在多線程環(huán)境下需要額外的同步機(jī)制來(lái)保證線程安全,這會(huì)大大降低其并發(fā)性能。例如,使用Collections.synchronizedMap方法對(duì)HashMap進(jìn)行包裝時(shí),會(huì)對(duì)所有的操作進(jìn)行同步處理,導(dǎo)致多個(gè)線程在操作HashMap時(shí)需要排隊(duì)等待,從而出現(xiàn)嚴(yán)重的性能瓶頸。
3.3 內(nèi)存占用
從內(nèi)存占用的角度來(lái)看,ConcurrentHashMap通常會(huì)比HashMap占用更多的內(nèi)存。這是因?yàn)镃oncurrentHashMap為了實(shí)現(xiàn)線程安全,需要額外的數(shù)據(jù)結(jié)構(gòu)和鎖機(jī)制。例如,在早期的版本中,ConcurrentHashMap使用分段鎖時(shí),每個(gè)段都需要占用一定的內(nèi)存空間。此外,ConcurrentHashMap在擴(kuò)容時(shí)也需要進(jìn)行更多的內(nèi)存分配和數(shù)據(jù)遷移操作,從而增加了內(nèi)存的占用。
相比之下,HashMap的內(nèi)存占用相對(duì)較少,因?yàn)樗恍枰~外的鎖機(jī)制和復(fù)雜的數(shù)據(jù)結(jié)構(gòu)。不過(guò),隨著ConcurrentHashMap的不斷優(yōu)化,其內(nèi)存占用情況也在逐漸改善。例如,在Java 8及以后的版本中,ConcurrentHashMap采用了新的數(shù)據(jù)結(jié)構(gòu)和優(yōu)化算法,減少了內(nèi)存的占用。
4. 使用場(chǎng)景
4.1 HashMap的使用場(chǎng)景
HashMap適用于單線程環(huán)境下的數(shù)據(jù)存儲(chǔ)和查詢場(chǎng)景。由于其非線程安全的特性,在單線程環(huán)境下可以提供較高的性能,且使用起來(lái)相對(duì)簡(jiǎn)單。以下是一些典型的使用場(chǎng)景:
- 數(shù)據(jù)緩存:在單線程應(yīng)用中,可以使用HashMap來(lái)緩存一些數(shù)據(jù),如數(shù)據(jù)庫(kù)查詢結(jié)果、計(jì)算結(jié)果等,以提高程序的執(zhí)行效率。
- 對(duì)象映射:在對(duì)象之間建立映射關(guān)系時(shí),可以使用HashMap來(lái)存儲(chǔ)對(duì)象的鍵值對(duì)。例如,在一個(gè)游戲中,可以使用HashMap來(lái)存儲(chǔ)玩家的ID和對(duì)應(yīng)的玩家對(duì)象之間的映射關(guān)系。
- 參數(shù)傳遞:在方法調(diào)用時(shí),可以使用HashMap來(lái)傳遞一組參數(shù),每個(gè)參數(shù)的名稱(chēng)作為鍵,參數(shù)的值作為值。這種方式可以簡(jiǎn)化參數(shù)的傳遞過(guò)程,使代碼更加靈活和可讀。
4.2 ConcurrentHashMap的使用場(chǎng)景
ConcurrentHashMap適用于多線程環(huán)境下的數(shù)據(jù)存儲(chǔ)和查詢場(chǎng)景。由于其線程安全的特性,在多線程環(huán)境下可以提供較高的性能和數(shù)據(jù)一致性保證。以下是一些典型的使用場(chǎng)景:
- 共享數(shù)據(jù)存儲(chǔ):在多線程應(yīng)用中,可以使用ConcurrentHashMap來(lái)存儲(chǔ)多個(gè)線程需要共享的數(shù)據(jù)。例如,在一個(gè)Web應(yīng)用中,可以使用ConcurrentHashMap來(lái)存儲(chǔ)用戶的會(huì)話信息,多個(gè)線程可以同時(shí)訪問(wèn)和修改這些會(huì)話信息,而不會(huì)出現(xiàn)數(shù)據(jù)不一致的問(wèn)題。
- 緩存實(shí)現(xiàn):在需要實(shí)現(xiàn)線程安全的緩存時(shí),可以使用ConcurrentHashMap作為底層數(shù)據(jù)結(jié)構(gòu)。例如,在一個(gè)分布式系統(tǒng)中,可以使用ConcurrentHashMap來(lái)緩存一些共享資源的狀態(tài)信息,多個(gè)節(jié)點(diǎn)可以同時(shí)訪問(wèn)和更新這些狀態(tài)信息。
- 并發(fā)統(tǒng)計(jì):在進(jìn)行并發(fā)統(tǒng)計(jì)時(shí),可以使用ConcurrentHashMap來(lái)存儲(chǔ)統(tǒng)計(jì)結(jié)果。例如,在一個(gè)日志分析系統(tǒng)中,可以使用ConcurrentHashMap來(lái)統(tǒng)計(jì)不同日志級(jí)別的出現(xiàn)次數(shù),多個(gè)線程可以同時(shí)對(duì)日志進(jìn)行分析和統(tǒng)計(jì),而不會(huì)出現(xiàn)統(tǒng)計(jì)結(jié)果不準(zhǔn)確的問(wèn)題。
5. 總結(jié)
在實(shí)際開(kāi)發(fā)中,我們需要根據(jù)具體的使用場(chǎng)景和需求來(lái)選擇合適的Map實(shí)現(xiàn)類(lèi)。如果是在單線程環(huán)境下,且需要處理可能存在null值的數(shù)據(jù),可以選擇使用HashMap;如果是在多線程環(huán)境下,需要保證數(shù)據(jù)的線程安全性和一致性,可以選擇使用ConcurrentHashMap。此外,還可以根據(jù)性能要求、內(nèi)存占用等因素來(lái)綜合考慮,以選擇最適合的Map實(shí)現(xiàn)類(lèi)來(lái)滿足實(shí)際需求。
到此這篇關(guān)于Java中的HashMap和ConcurrentHashMap區(qū)別和適用場(chǎng)景的文章就介紹到這了,更多相關(guān)HashMap和ConcurrentHashMap的區(qū)別內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
如何擴(kuò)展Spring Cache實(shí)現(xiàn)支持多級(jí)緩存
這篇文章主要介紹了如何擴(kuò)展Spring Cache實(shí)現(xiàn)支持多級(jí)緩存,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-11-11使用MyBatis返回其它類(lèi)對(duì)象的字段處理
這篇文章主要介紹了使用MyBatis返回其它類(lèi)對(duì)象的字段處理方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-08-08Java獲取一維數(shù)組的最小值實(shí)現(xiàn)方法
這篇文章主要介紹了Java獲取一維數(shù)組的最小值實(shí)現(xiàn)方法,需要的朋友可以參考下2014-02-02response.setContentType()參數(shù)以及作用詳解
這篇文章主要介紹了response.setContentType()參數(shù)以及作用詳解,本篇文章通過(guò)簡(jiǎn)要的案例,講解了該項(xiàng)技術(shù)的了解與使用,以下就是詳細(xì)內(nèi)容,需要的朋友可以參考下2021-08-08Spring如何動(dòng)態(tài)自定義logback日志目錄詳解
這篇文章主要給大家介紹了關(guān)于Spring如何動(dòng)態(tài)自定義logback日志目錄的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2018-10-10Java實(shí)現(xiàn)數(shù)組去除重復(fù)數(shù)據(jù)的方法詳解
這篇文章主要介紹了Java實(shí)現(xiàn)數(shù)組去除重復(fù)數(shù)據(jù)的方法,結(jié)合實(shí)例形式詳細(xì)分析了java數(shù)組去除重復(fù)的幾種常用方法、實(shí)現(xiàn)原理與相關(guān)注意事項(xiàng),需要的朋友可以參考下2017-09-09使用Java進(jìn)行Json數(shù)據(jù)的解析(對(duì)象數(shù)組的相互嵌套)
下面小編就為大家?guī)?lái)一篇使用Java進(jìn)行Json數(shù)據(jù)的解析(對(duì)象數(shù)組的相互嵌套)。小編覺(jué)得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-08-08淺析Java虛擬機(jī)詳解之概述、對(duì)象生存法則
這篇文章主要介紹了Java虛擬機(jī)詳解之概述、對(duì)象生存法則,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-04-04