Java中HashMap的用法詳細(xì)介紹
一.HashMap
1.基本概念
HashMap是基于哈希表實(shí)現(xiàn)的Map接口,用于存儲(chǔ)鍵值對(duì)(K-V)格式,其核心作用就是通過哈希函數(shù)為了更快查詢到某個(gè)元素,時(shí)間復(fù)雜度O(1);
2.底層數(shù)據(jù)結(jié)構(gòu):
在java(1.7)之前 底層是采用“數(shù)組 + 鏈表” 的形式存儲(chǔ)
但在java(1.8) 之后,變成了“數(shù)組 + 鏈表 + 紅黑樹 ”形式存儲(chǔ)
我們主要了解java1.8之后的內(nèi)容;
從源碼上查看
數(shù)組的初始容量為16,負(fù)載因子為0.75,所以當(dāng)超過這個(gè)閾值 16 * 0.75 = 12
設(shè)計(jì)初始容量為16核心原因是2 的冪次方更適合哈希計(jì)算,能減少哈希沖突,但8又太頻繁擴(kuò)容,新增性能開銷,而32空間又會(huì)太大,則浪費(fèi)空間~
負(fù)載因子也是同理,太小,會(huì)頻繁擴(kuò)容,雖然查詢快了,但是數(shù)組太稀疏,浪費(fèi)空間,而太大,就會(huì)擴(kuò)容少,空間利用率高,但查詢會(huì)變慢~
當(dāng)插入第13個(gè)元素的時(shí)候,已經(jīng)超過閾值,為了避免哈希沖突,會(huì)進(jìn)行擴(kuò)容~
然后這邊當(dāng)數(shù)組大小超過64時(shí)候,鏈表大小超過8的時(shí)候,會(huì)從鏈表進(jìn)化成紅黑樹,但當(dāng)元素個(gè)數(shù)少于6個(gè)時(shí),會(huì)退回鏈表~
鏈表長度≥8
基于泊松分布,當(dāng)負(fù)載因子為 0.75 時(shí),鏈表長度自然增長到 8 的概率極低(約千萬分之一),此時(shí)說明哈希沖突異常頻繁,需要轉(zhuǎn)為紅黑樹優(yōu)化。數(shù)組容量≥64
若數(shù)組容量小于 64(如 32),即使鏈表長度≥8,也會(huì)先觸發(fā)擴(kuò)容(而非轉(zhuǎn)紅黑樹)。原因是:數(shù)組容量小時(shí),擴(kuò)容成本低,通過擴(kuò)容可分散元素,減少?zèng)_突;而紅黑樹的維護(hù)成本(插入 / 刪除時(shí)的旋轉(zhuǎn)操作)高于擴(kuò)容,此時(shí)擴(kuò)容更高效。
3.HashCode和equals方法
HashMap中的鍵一定會(huì)實(shí)現(xiàn)這倆個(gè)方法,hashCode用于計(jì)算存儲(chǔ)的位置,而equals用于判斷來個(gè)鍵是否相同,在put方法的時(shí)候,如果倆個(gè)哈希值相同,會(huì)判斷是否是同一個(gè)值,如果不是就會(huì)放在同一個(gè)桶中的不同位置,如果相同就是一個(gè)東西~
為什么重寫HashCode方法?
如果不重寫,這個(gè)時(shí)候,就會(huì)導(dǎo)致倆個(gè)相同的key,會(huì)被計(jì)算出倆個(gè)不同的哈希值,導(dǎo)致他們存在在不同的桶中,到時(shí)候查詢的時(shí)候到底是查哪個(gè)?
為什么重新equals方法?
equals方法是為了當(dāng)出現(xiàn)哈希沖突的時(shí)候,倆個(gè)key的哈希值相同,放在同一桶中,但沒有比較它們的對(duì)象,可能會(huì)誤判會(huì)導(dǎo)致倆個(gè)key存儲(chǔ)在不同位置,所以沒有覆蓋之前的值,就會(huì)錯(cuò)誤~
class Person { String name; int age; @Override public int hashCode() { // 重寫hashCode,保證邏輯相等的對(duì)象哈希值相同 return Objects.hash(name, age); } // 未重寫equals() } public static void main(String[] args) { HashMap<Person, String> map = new HashMap<>(); Person p1 = new Person("張三", 20); Person p2 = new Person("張三", 20); map.put(p1, "學(xué)生"); map.put(p2, "工人"); // 哈希值相同,進(jìn)入同一個(gè)桶,但equals判斷為不同key System.out.println(map.size()); // 仍輸出2,而非預(yù)期的1 }
hashCode()
和equals()
必須配套重寫,且滿足以下規(guī)則:
- 一致性:如果兩個(gè)對(duì)象通過
equals()
判斷為相等,則它們的hashCode()
必須返回相同的值; - 對(duì)稱性:如果
a.equals(b)
為true
,則b.equals(a)
也必須為true
; - 非空性:
a.equals(null)
必須返回false
。
4.put操作
1.初始化和數(shù)組檢查
首先判斷 HashMap 是否未初始化(table
數(shù)組為 null
或長度為 0),若是則觸發(fā) 初始化(resize):
2.計(jì)算索引并檢查桶是否為空
如果該位置為空,直接創(chuàng)建新節(jié)點(diǎn)插入
3.桶不為null,處理哈希沖突
- 檢查首節(jié)點(diǎn):若首節(jié)點(diǎn)的 key 與傳入 key equals 相等(且哈希值相同),則視為 “重復(fù) key”,記錄該節(jié)點(diǎn)(后續(xù)替換其 value)。
- 遍歷后續(xù)節(jié)點(diǎn):
- 若桶是單鏈表(
Node
):遍歷鏈表,若找到 key 相等的節(jié)點(diǎn),記錄該節(jié)點(diǎn);若遍歷到鏈表尾部仍未找到,則在鏈尾插入新節(jié)點(diǎn)(Node
)。 - 若桶是紅黑樹(
TreeNode
):調(diào)用紅黑樹的插入方法(putTreeVal
),若找到重復(fù) key 則記錄節(jié)點(diǎn),否則插入新樹節(jié)點(diǎn)。
- 若桶是單鏈表(
- 處理重復(fù) key:若步驟 1 或 2 中找到重復(fù) key 的節(jié)點(diǎn),用新 value 替換其舊 value,流程結(jié)束。
4.判斷鏈表是否轉(zhuǎn)化紅黑樹
5.以及數(shù)組大小是否超過閾值進(jìn)行擴(kuò)容
5.get操作
同理get
方法的核心邏輯是:
哈希計(jì)算→定位桶→根據(jù)桶結(jié)構(gòu)(鏈表 / 紅黑樹)查找匹配節(jié)點(diǎn)
java1.7 -- java1.8 HashMap 做了哪些優(yōu)化:
哈希值計(jì)算:
1.7版本:對(duì) key 的原始哈希值(hashCode() 結(jié)果)進(jìn)行 4 次擾動(dòng)(多次移位和異或運(yùn)算)
1.8版本:簡化為 1 次擾動(dòng),僅通過一次 “高 16 位與低 16 位異或” 實(shí)現(xiàn)混合
減少計(jì)算開銷:一次異或運(yùn)算即可達(dá)到近似的混合效果,相比 1.7 的 4 次運(yùn)算,計(jì)算效率更高。
實(shí)際效果:在大多數(shù)場(chǎng)景下,1 次擾動(dòng)已能保證哈希值的均勻分布,同時(shí)降低了 CPU 運(yùn)算成本。
鏈表插入方式:
由頭插變成尾插,核心是為了解決多線程擴(kuò)容時(shí)的鏈表環(huán)化問題,同時(shí)讓鏈表操作邏輯更合理。
多線程擴(kuò)容時(shí)可能導(dǎo)致鏈表環(huán)化(死循環(huán))。
擴(kuò)容過程中,節(jié)點(diǎn)會(huì)從舊數(shù)組遷移到新數(shù)組,頭插法在遷移時(shí)會(huì)反轉(zhuǎn)鏈表順序(例如舊鏈表 A→B,遷移后新鏈表變?yōu)?B→A)。若此時(shí)有多個(gè)線程同時(shí)操作,可能出現(xiàn)節(jié)點(diǎn)引用相互指向的情況(如 A.next = B 且 B.next = A),形成環(huán)形鏈表。后續(xù)查詢時(shí),線程會(huì)陷入無限循環(huán),導(dǎo)致 CPU 占用飆升。
二.ConcurrentHashMap
ConcurrentHashMap 是 Java 中用于并發(fā)場(chǎng)景的哈希表實(shí)現(xiàn),專為高并發(fā)環(huán)境設(shè)計(jì);
1.Java 1.7 ConcurrentHashMap架構(gòu):
在java1.8之前 ConcurrentHashMap 采用的是 分段數(shù)組(Segment)+ 哈希表 , 默認(rèn)為16個(gè)segment,同時(shí)支持16個(gè)并發(fā)~
- 整體結(jié)構(gòu):
ConcurrentHashMap -> Segment[] -> HashEntry[] -> 鏈表
。
- 寫操作(put/remove 等):
- 計(jì)算 key 的哈希值,定位到對(duì)應(yīng)的
Segment
; - 獲取該
Segment
的鎖(若被占用則阻塞); - 在
Segment
內(nèi)部的哈希表中執(zhí)行插入 / 刪除(類似 HashMap 的邏輯,鏈表頭插法); - 釋放鎖。
- 計(jì)算 key 的哈希值,定位到對(duì)應(yīng)的
- 優(yōu)點(diǎn):通過分段鎖實(shí)現(xiàn)了多線程并發(fā)寫,效率比全表鎖(如 Hashtable)高得多。
- 缺點(diǎn):
- 鎖粒度仍較大(以
Segment
為單位),若多個(gè)線程操作同一Segment
,仍會(huì)阻塞; - 結(jié)構(gòu)復(fù)雜,維護(hù)成本高;
- 擴(kuò)容僅針對(duì)單個(gè)
Segment
,但整體性能受限于Segment
數(shù)量。
- 鎖粒度仍較大(以
2.Java 1.8 ConcurrentHashMap架構(gòu):
摒棄了 Segment
分段結(jié)構(gòu),底層直接使用 數(shù)組 + 鏈表 + 紅黑樹(與 JDK 1.8 HashMap 結(jié)構(gòu)類似)
鎖的粒度更小,鎖的是桶的頭節(jié)點(diǎn),并且采取了CAS + synchronized 機(jī)制,僅當(dāng)多個(gè)線程操作同一鏈表 / 紅黑樹時(shí)才會(huì)競(jìng)爭鎖,鎖沖突概率遠(yuǎn)低于 1.7 的 Segment 級(jí)鎖。
CAS+synchronized的使用場(chǎng)景:
- 無沖突場(chǎng)景(空桶插入、初始化、低并發(fā)計(jì)數(shù)):用 CAS 實(shí)現(xiàn)高效無鎖操作。
- 有沖突場(chǎng)景(非空桶操作、復(fù)雜結(jié)構(gòu)修改):用 synchronized 加鎖保證原子性。
版本 | 底層架構(gòu) | 哈希表數(shù)量 | 鎖粒度 |
---|---|---|---|
JDK 1.7 及之前 | 兩層結(jié)構(gòu):Segment [] 數(shù)組 + 每個(gè) Segment 包含一個(gè) HashEntry [] 數(shù)組 | 多個(gè)(默認(rèn) 16 個(gè),與 Segment 數(shù)量一致) | Segment 級(jí)(鎖整個(gè)子哈希表) |
JDK 1.8 及之后 | 單層結(jié)構(gòu):Node [] 數(shù)組(鏈表 / 紅黑樹解決沖突) | 1 個(gè)(整個(gè) ConcurrentHashMap 共用一個(gè)底層數(shù)組) | 節(jié)點(diǎn)級(jí)(鎖鏈表頭節(jié)點(diǎn)或紅黑樹根節(jié)點(diǎn)) |
擴(kuò)容區(qū)別:
特性 | JDK 1.7 擴(kuò)容 | JDK 1.8 擴(kuò)容 |
---|---|---|
擴(kuò)容范圍 | 單個(gè) Segment 獨(dú)立擴(kuò)容 | 整個(gè)數(shù)組全表擴(kuò)容 |
并發(fā)能力 | 單線程擴(kuò)容(當(dāng)前操作 Segment 的線程) | 多線程協(xié)同擴(kuò)容(所有線程可協(xié)助遷移) |
鎖影響范圍 | 僅鎖定當(dāng)前 Segment,其他 Segment 可用 | 無全局鎖,僅鎖定遷移中的桶節(jié)點(diǎn) |
遷移效率 | 單個(gè) Segment 遷移,效率較低 | 多線程分片遷移,效率更高 |
節(jié)點(diǎn)遷移方式 | 頭插法(可能反轉(zhuǎn)鏈表) | 尾插法(保持鏈表順序,無環(huán)化風(fēng)險(xiǎn)) |
與讀寫的兼容性 | 擴(kuò)容時(shí)該 Segment 讀寫阻塞 | 擴(kuò)容與讀寫可并行(讀無鎖,寫鎖單個(gè)桶) |
size()區(qū)別:
在JDK 1.7 中,ConcurrentHashMap 由多個(gè) Segment
組成,每個(gè) Segment
獨(dú)立維護(hù)自己的元素?cái)?shù)量(count
),因此計(jì)算總 size
時(shí)需要累加所有 Segment 的 count。
- 為減少誤差,JDK 1.7 采用 “重試機(jī)制”:如果兩次連續(xù)累加的結(jié)果一致,則認(rèn)為是準(zhǔn)確值;若不一致(說明期間有并發(fā)修改),最多重試 3 次,若仍不一致則直接返回當(dāng)前累加值(接受一定誤差)。
而在JDK 1.8中,計(jì)算 size
依賴于 baseCount
原子變量和 counterCells
輔助數(shù)組,通過無鎖累加實(shí)現(xiàn),返回兩者的總和作為最終的 size
。
當(dāng)插入元素成功后,需要將總數(shù) +1
,流程如下:
優(yōu)先嘗試更新 baseCount:
線程通過 CAS 操作直接更新baseCount
(baseCount + 1
)。- 若 CAS 成功:計(jì)數(shù)完成,無需其他操作。
- 若 CAS 失?。赫f明存在并發(fā)競(jìng)爭(多個(gè)線程同時(shí)更新
baseCount
),進(jìn)入下一步。
競(jìng)爭激烈時(shí),使用 counterCells 分散計(jì)數(shù):
- 若
counterCells
未初始化,先通過 CAS 初始化(創(chuàng)建一個(gè)CounterCell
數(shù)組)。 - 線程通過哈希算法(基于線程 ID 或隨機(jī)數(shù))隨機(jī)選擇
counterCells
中的一個(gè)CounterCell
。 - 嘗試用 CAS 更新該
CounterCell
的value
(value + 1
):- 若成功:計(jì)數(shù)完成。
- 若失?。褐卦嚮蜻x擇其他
CounterCell
(避免死鎖)。
- 若
特性 | JDK 1.7 計(jì)數(shù)方式 | JDK 1.8 計(jì)數(shù)方式 |
---|---|---|
底層依賴 | 各 Segment 的 count 累加 | baseCount + counterCells 累加 |
并發(fā)處理 | 重試機(jī)制減少誤差,返回近似值 | CAS 原子操作,返回接近精確值 |
性能 | 低并發(fā)時(shí)高效,依賴 Segment 數(shù)量 | 高低并發(fā)均高效,通過分散競(jìng)爭優(yōu)化 |
實(shí)現(xiàn)復(fù)雜度 | 簡單(遍歷 + 重試) | 復(fù)雜(原子變量 + 輔助數(shù)組 + 競(jìng)爭分散) |
適用場(chǎng)景 | 分段鎖架構(gòu)下的近似計(jì)數(shù) | 全局?jǐn)?shù)組架構(gòu)下的高效精確計(jì)數(shù) |
總結(jié)
到此這篇關(guān)于Java中HashMap用法詳細(xì)介紹的文章就介紹到這了,更多相關(guān)java hashmap詳解內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
java實(shí)現(xiàn)ftp上傳 如何創(chuàng)建文件夾
這篇文章主要為大家詳細(xì)介紹了java實(shí)現(xiàn)ftp上傳的相關(guān)資料,教大家如何創(chuàng)建文件夾?具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-04-04springboot下添加日志模塊和設(shè)置日志文件輸出的方法
日志的使用將通過SLF4J來使用,SLF4J是一個(gè)為Java應(yīng)用提供簡單日志記錄的接口,在Spring框架中,SLF4J常常用于處理框架本身以及應(yīng)用程序的日志記錄,本文給大家介紹springboot下添加日志模塊和設(shè)置日志文件輸出的相關(guān)知識(shí),感興趣的朋友一起看看吧2023-12-12idea使用mybatis插件mapper中的方法爆紅的解決方案
這篇文章主要介紹了idea使用mybatis插件mapper中的方法爆紅的解決方案,文中給出了詳細(xì)的原因分析和解決方案,對(duì)大家解決問題有一定的幫助,需要的朋友可以參考下2024-07-07Java實(shí)現(xiàn)學(xué)生管理系統(tǒng)
這篇文章主要為大家詳細(xì)介紹了Java實(shí)現(xiàn)學(xué)生管理系統(tǒng),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-01-01詳解SpringBoot 應(yīng)用如何提高服務(wù)吞吐量
這篇文章主要介紹了Spring Boot 應(yīng)用如何提高服務(wù)吞吐量,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-07-07詳解Spring的autowire-candidate設(shè)計(jì)
現(xiàn)在的Spring應(yīng)用通常都是基于注解開發(fā),但是對(duì)Spring感興趣的同學(xué)可以借助Spring早期基于Xml配置的各種運(yùn)用來加深對(duì)Spring框架內(nèi)部的理解和體會(huì)Spring框架的設(shè)計(jì)之妙。這篇文章我們就來談?wù)刋ml配置之default-autowire-candidates2021-06-06