HashMap以及其子類關(guān)鍵性總結(jié)


HashMap

1.7中的HashMap

負(fù)載因子: 給定默認(rèn)容量為16 負(fù)載因子為0.75

  • Map在使用過程中不斷的往里面存放元素 當(dāng)數(shù)量達(dá)到16*0.75=12時(shí) 就需要將當(dāng)前的默認(rèn)容量擴(kuò)容 而擴(kuò)容的過程設(shè)計(jì)reHash 復(fù)制數(shù)組等操作,非常損耗性能
  • 建議提前預(yù)估HashMap的初始大小,盡量減少HashMap的擴(kuò)容帶來的性能損耗

其實(shí)真正存放數(shù)據(jù)的是 Entry<K,V>[] table,Entry 是 HashMap 中的一個(gè)靜態(tài)內(nèi)部類轻腺,它有key梦皮、value挠蛉、next堂湖、hash(key的hashcode)成員變量 多個(gè)Entry就構(gòu)成hashMap的數(shù)據(jù)結(jié)構(gòu) 數(shù)組+鏈表

put()
  • 判斷當(dāng)前的數(shù)組是否需要初始化
  • 如果Key為Null 則put一個(gè)空值進(jìn)去
  • 根據(jù)key計(jì)算出hashCode
  • 根據(jù)計(jì)算出來的HashCode定位出所在的數(shù)組位置(頭插法)
  • 如果數(shù)組位置是一個(gè)鏈表則需要遍歷判斷里面的HashCode key 是否和傳入的相等, 如果相同就進(jìn)行覆蓋,并且返回被覆蓋的值
  • 如果這個(gè)數(shù)組位置是空的,new 一個(gè)Entry對(duì)象寫入到當(dāng)前位置,(調(diào)用addEnrty 寫入Entry時(shí)需要判斷是否需要擴(kuò)容,擴(kuò)容就是2倍 并將當(dāng)前的key 重新hash并定位, 而在createEntry中將當(dāng)前位置的數(shù)組算元傳入到新建的數(shù)組位置中 如果當(dāng)前數(shù)組位置有值就會(huì)形成鏈表)

get()

  • 根據(jù)計(jì)算出來的HashCode找倒對(duì)應(yīng)的HashEnrty位置 如果在數(shù)組上直接返回
  • 判斷是否為鏈表 不是鏈表就根據(jù)Key .key以及HashCode 是否相等來返回
  • 為鏈表的需要遍歷到key以及hashcode相等再返回
  • 啥都沒有取到就返回Null
1.8中的HashMap

當(dāng)Hash沖突嚴(yán)重時(shí),在桶上形成的鏈表越來越長(zhǎng),這樣在查詢時(shí)的效率就會(huì)越來越低,時(shí)間復(fù)雜度為o(N)

TREEIFY_THRESHOLD = 8 用于判斷是否將鏈表轉(zhuǎn)為紅黑樹的閾值

桶中存放的數(shù)據(jù)結(jié)構(gòu)為Node

put()
  • 判斷當(dāng)前桶是否為空 空就需要初始化(resize方法中會(huì)判斷是否進(jìn)行初始化)
  • 根據(jù)當(dāng)前的key的hashcode定位到具體的桶中并且判斷桶是否為空,為空表名沒有Hash沖突直接在當(dāng)前位置新建一個(gè)桶即可
  • 如果存在Hash沖突 那么就要比較桶中的key.key的hashCode與寫入的key是相等,如果相等.就賦值給e,等待循環(huán)結(jié)束后替換
  • 如果當(dāng)前桶是個(gè)紅黑樹 那就要按照紅黑樹的范式寫入數(shù)據(jù)
  • 如果是一個(gè)鏈表就需要將當(dāng)前的key value封裝成一個(gè)新節(jié)點(diǎn)Node寫入到當(dāng)前桶的第一個(gè)位置 (尾插法)
  • 接著判斷當(dāng)前鏈表大小是否大于預(yù)設(shè)的鏈表閾值 大于就轉(zhuǎn)成紅黑樹
  • 如果遍歷過程中找到key相同值時(shí)直接退出遍歷
  • 如果e!=null 就相當(dāng)于存在相同的key 就替換存在的值 返回原值
  • 最后判斷是否需要進(jìn)行擴(kuò)容
get()
  • 根據(jù)計(jì)算出來的HashCode找倒對(duì)應(yīng)桶的位置 如果在數(shù)組上直接返回 如果為空返回null
  • 如果第一個(gè)不匹配則判斷它的下一個(gè)是紅黑樹還是鏈表
  • 紅黑樹就按照樹的查找方法返回
  • 鏈表就需要遍歷匹配返回值
但是 HashMap 原有的問題也都存在,比如在并發(fā)場(chǎng)景下使用時(shí)容易出現(xiàn)死循環(huán)
  • 在 HashMap 擴(kuò)容的時(shí)候會(huì)調(diào)用 resize() 方法侦讨,就是這里的并發(fā)操作容易在一個(gè)桶上形成環(huán)形鏈表窘面;這樣當(dāng)獲取一個(gè)不存在的 key 時(shí),計(jì)算出的 index 正好是環(huán)形鏈表的下標(biāo)就會(huì)出現(xiàn)死循環(huán):在 1.7 中 hash 沖突采用的頭插法形成的鏈表件相,在并發(fā)條件下會(huì)形成循環(huán)鏈表再扭,一旦有查詢落到了這個(gè)鏈表上,當(dāng)獲取不到值時(shí)就會(huì)死循環(huán)夜矗。
HashMap何時(shí)擴(kuò)容

當(dāng)向容器添加元素的時(shí)候泛范,會(huì)判斷當(dāng)前容器的元素個(gè)數(shù),如果大于等于閾值(默認(rèn)12)---即大于當(dāng)前數(shù)組的長(zhǎng)度乘以加載因子的值的時(shí)候紊撕,就要自動(dòng)擴(kuò)容罢荡。

HashMap擴(kuò)容算法

擴(kuò)容(resize) 就是重新計(jì)算容量,數(shù)組無法自動(dòng)擴(kuò)容 方法就是使用一個(gè)新數(shù)組代替已有的容量小的數(shù)組 2倍擴(kuò)容

Hashmap如何解決散列碰撞

HashMap是利用拉鏈法 處理hashcode的碰撞問題 在調(diào)用HashMap的put或者get方法時(shí),都會(huì)調(diào)用Hashcode方法區(qū)查找相關(guān)的key 當(dāng)有沖突時(shí)在調(diào)用equals方法

HashMap基于hashing原理 通過put和get方法存取對(duì)象,當(dāng)我們將鍵值對(duì)傳遞給put方法時(shí),他調(diào)用對(duì)象的hashCode方法計(jì)算Hashcode 知道哦啊哦哈系統(tǒng)位置來存儲(chǔ)對(duì)象,當(dāng)獲取對(duì)象時(shí),通過鍵對(duì)象的equals()方法找到正確的鍵值對(duì)对扶,然后返回值對(duì)象区赵。

HashMap使用鏈表來解決碰撞問題,當(dāng)碰撞發(fā)生了浪南,對(duì)象將會(huì)存儲(chǔ)在鏈表的下一個(gè)節(jié)點(diǎn)中笼才。hashMap在每個(gè)鏈表節(jié)點(diǎn)存儲(chǔ)鍵值對(duì)對(duì)象。當(dāng)兩個(gè)不同的鍵卻有相同的hashCode時(shí)络凿,他們會(huì)存儲(chǔ)在同一個(gè)bucket位置的鏈表中骡送。鍵對(duì)象的equals()來找到鍵值對(duì)絮记。

Hashmap底層為什么是線程不安全的摔踱?
  • 并發(fā)場(chǎng)景下使用時(shí)容易出現(xiàn)死循環(huán) 在 HashMap 擴(kuò)容的時(shí)候會(huì)調(diào)用 resize() 方法,就是這里的并發(fā)操作容易在一個(gè)桶上形成環(huán)形鏈表怨愤;這樣當(dāng)獲取一個(gè)不存在的 key 時(shí)派敷,計(jì)算出的 index 正好是環(huán)形鏈表的下標(biāo)就會(huì)出現(xiàn)死循環(huán)
  • 在 1.7 中 hash 沖突采用的頭插法形成的鏈表,在并發(fā)條件下會(huì)形成循環(huán)鏈表,一旦有查詢落到了這個(gè)鏈表上篮愉,當(dāng)獲取不到值時(shí)就會(huì)死循環(huán)般眉。

ConcurrentHashMap

1.7 中ConcurrentHashMap

ConcurrentHashMap在jdk1.7中使用了分段鎖,其中segment繼承于ReentranLock ,不會(huì)像HashTable那樣不管是put還是get都去做同步處理,理論上ConcurrentHashMap支持CurrencyLevel(Segment數(shù)組數(shù)量)的線程并發(fā),當(dāng)每個(gè)線程占用鎖訪問一個(gè)Segment時(shí),不會(huì)影響到其他的Segment

數(shù)據(jù)結(jié)構(gòu)和HashMap一致 數(shù)組+鏈表

分段鎖

ConcurrentHashMap里面元素存放在table數(shù)組中,分段鎖就是把這個(gè)table分成多段,每一把鎖用于鎖容器其中一部分?jǐn)?shù)據(jù),那么當(dāng)多線程訪問容器里不同數(shù)據(jù)段的數(shù)據(jù)時(shí)潜支,線程間就不會(huì)存在鎖競(jìng)爭(zhēng)甸赃,從而可以有效的提高并發(fā)訪問效率,在ConcurrentHashMap中使用了一個(gè)包含16個(gè)鎖的數(shù)組,每個(gè)鎖保護(hù)所有散列桶的1/16冗酿,其中第N個(gè)散列桶由第(N mod 16)個(gè)鎖來保護(hù)埠对。假設(shè)使用合理的散列算法使關(guān)鍵字能夠均勻的分部,那么這大約能使對(duì)鎖的請(qǐng)求減少到越來的1/16裁替。也正是這項(xiàng)技術(shù)使得ConcurrentHashMap支持多達(dá)16個(gè)并發(fā)的寫入線程项玛。

put()

首先通過Key定位到Segment,之后再對(duì)應(yīng)的Segment中具體的put元素

  • 雖然HashEntry中的Value是用Volatile關(guān)鍵字修飾,但是并不能保證并發(fā)的原子性,所以put操作時(shí)任然需要加鎖處理
  • 首先第一步的時(shí)候會(huì)去嘗試獲取鎖, 如果鎖獲取失敗肯定就存在與其他線程存在競(jìng)爭(zhēng),利用scanAdnLockForPut()自選獲取鎖,如果獲取次數(shù)達(dá)到了Max_SCAN_RETRIES,則改為阻塞鎖獲取,保證能獲取鎖成功
  • 將當(dāng)前的Segment中的table通過key的hashCode定位到HashEntry
  • 遍歷該HashEntry 如果不為空則判斷傳入的key和當(dāng)前遍歷的Key是否相等,相等則覆蓋舊的Key
  • 為空則需要新建一個(gè)HashEntry并加入到Segment中,同時(shí)還會(huì)判斷是否需要擴(kuò)容
  • 最后會(huì)使用unLock解除當(dāng)前Segment的鎖
get()
  • 只需要將Key通過 Hash之后定位到具體的Segment 再通過一次Hash定位到元素上
  • 由于HashEntry 中的Value屬性是用Volitile 關(guān)鍵字修飾,保證了內(nèi)存的可見性,所以每次都可以實(shí)時(shí)獲取最新值
  • ConCurrentHashMap的get方法是相對(duì)高效的,因?yàn)檎麄€(gè)過程不需要加鎖

1.8中的ConCurrentHashMap

在1.7中已經(jīng)解決了HashMap的并發(fā)問題 ,并且支持N個(gè)Segment這個(gè)多次數(shù)的并發(fā),但是任然存在1.7中HashMap的問題,查詢遍歷鏈表效率低, 和1.8中的HashMap結(jié)構(gòu)類似, 其中拋棄可原有的分段鎖,采用了CAS+Synchronized來保證并發(fā)的安全

CAS

如果Obj內(nèi)的Value和Expect相同 就證明沒有其他線程改變過這個(gè)變量,name就更新它為update 如果這一步的CAS沒有成功,那么就采用自選的方式繼續(xù)進(jìn)行CAS操作

對(duì)CAS的理解,CAS是一種無鎖算法弱判,CAS有3個(gè)操作數(shù)襟沮,內(nèi)存值V,舊的預(yù)期值A(chǔ)昌腰,要修改的新值B开伏。當(dāng)且僅當(dāng)預(yù)期值A(chǔ)和內(nèi)存值V相同時(shí),將內(nèi)存值V修改為B遭商,否則什么都不做

CAS ABA問題
  • 問題描述 現(xiàn)在在對(duì)象中存入一Value A 但是另一個(gè)線程把Value的值從A改到B 又從B重新變?yōu)锳 'eg :插進(jìn)去 取出來等于沒插?'
    • 解決 目前在JDK的atomic包里提供了一個(gè)類AtomicStampedReference來解決ABA問題,這個(gè)類的compareAndSet方法作用是首先檢查當(dāng)前引用是否等于預(yù)期引用固灵,并且當(dāng)前標(biāo)志是否等于預(yù)期標(biāo)志,如果全部相等劫流,則以原子方式將該引用和該標(biāo)志的值設(shè)置為給定的更新值巫玻。
  • 如果CAS不成功 則會(huì)原地自旋,時(shí)間長(zhǎng)會(huì)給CPU帶來巨大開銷
  • 其實(shí)除了AtomicStampedReference類,還有一個(gè)原子類也可以解決祠汇,就是AtomicMarkableReference仍秤,它不是維護(hù)一個(gè)版本號(hào),而是維護(hù)一個(gè)boolean類型的標(biāo)記可很,用法沒有AtomicStampedReference靈活诗力。因此也只是在特定的場(chǎng)景下使用。

數(shù)據(jù)結(jié)構(gòu)和HashMap一致 數(shù)組+鏈表+紅黑樹

put()
  • 根據(jù)Key計(jì)算出HashCode
  • 判斷當(dāng)前數(shù)組table是否需要初始化
  • 如果當(dāng)前Key定位出的Node為空 表示可以寫入數(shù)據(jù),利用CAS嘗試寫入,失敗則自旋保證成功
  • 如果當(dāng)前位置的Hashcode==MOVE==-1 則需要擴(kuò)容
  • 如果都不滿足 則利用Synchronized鎖寫入數(shù)據(jù)
  • 最后判斷如果數(shù)組長(zhǎng)度大于TREEIFY_THRESHOLD(8) 則要轉(zhuǎn)化成紅黑樹
get()
  • 根據(jù)計(jì)算出來的HashCode找倒對(duì)應(yīng)的NodeBin位置 如果在數(shù)組上直接返回
  • 如果是紅黑樹就按照數(shù)的遍歷方式來獲取
  • 如果都不滿足 就按照鏈表的方式遍歷獲取值

1.8 在 1.7 的數(shù)據(jù)結(jié)構(gòu)上做了大的改動(dòng)根穷,采用紅黑樹之后可以保證查詢效率(O(logn))姜骡,甚至取消了 ReentrantLock(可重入鎖) 改為了 synchronized(獨(dú)占鎖),這樣可以看出在新版的 JDK 中對(duì) synchronized 優(yōu)化是很到位的屿良。


ArrayMap

ArrayMap利用兩個(gè)數(shù)組,mHashes用來保存每一個(gè)key的hash值惫周,mArrray大小為mHashes的2倍尘惧,依次保存key和value

mHashes[index] = hash;
mArray[index<<1] = key;
mArray[(index<<1)+1] = value;

當(dāng)插入時(shí),根據(jù)key的hashcode()方法得到hash值递递,計(jì)算出在mArrays的index位置喷橙,然后利用二分查找找到對(duì)應(yīng)的位置進(jìn)行插入啥么,當(dāng)出現(xiàn)哈希沖突時(shí),會(huì)在index的相鄰位置插入贰逾。


SparseArray

SparseArray比HashMap更省內(nèi)存悬荣,在某些條件下性能更好,主要是因?yàn)樗苊饬藢?duì)key的自動(dòng)裝箱(int轉(zhuǎn)為Integer類型)疙剑,它內(nèi)部則是通過兩個(gè)數(shù)組來進(jìn)行數(shù)據(jù)存儲(chǔ)的氯迂,一個(gè)存儲(chǔ)key,另外一個(gè)存儲(chǔ)value言缤,為了優(yōu)化性能嚼蚀,它內(nèi)部對(duì)數(shù)據(jù)還采取了壓縮的方式來表示稀疏數(shù)組的數(shù)據(jù),從而節(jié)約內(nèi)存空間管挟,我們從源碼中可以看到key和value分別是用數(shù)組表示:

private int[] mKeys; 
private Object[] mValues;

同時(shí)轿曙,SparseArray在存儲(chǔ)和讀取數(shù)據(jù)時(shí)候,使用的是二分查找法僻孝。也就是在put添加數(shù)據(jù)的時(shí)候导帝,會(huì)使用二分查找法和之前的key比較當(dāng)前我們添加的元素的key的大小,然后按照從小到大的順序排列好穿铆,所以舟扎,SparseArray存儲(chǔ)的元素都是按元素的key值從小到大排列好的。 而在獲取數(shù)據(jù)的時(shí)候悴务,也是使用二分查找法判斷元素的位置睹限,所以,在獲取數(shù)據(jù)的時(shí)候非逞堕埽快羡疗,比HashMap快的多。

假設(shè)數(shù)據(jù)量都在千級(jí)以內(nèi)的情況下:
  1. 如果key的類型已經(jīng)確定為int類型别洪,那么使用SparseArray叨恨,因?yàn)樗苊饬俗詣?dòng)裝箱的過程,

  2. 如果key為long類型挖垛,它還提供了一個(gè)LongSparseArray來確保key為long類型時(shí)的使用

  3. 如果key類型為其它的類型痒钝,則使用ArrayMap。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末痢毒,一起剝皮案震驚了整個(gè)濱河市送矩,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌哪替,老刑警劉巖栋荸,帶你破解...
    沈念sama閱讀 206,311評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡晌块,警方通過查閱死者的電腦和手機(jī)爱沟,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,339評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來匆背,“玉大人呼伸,你說我怎么就攤上這事《凼” “怎么了括享?”我有些...
    開封第一講書人閱讀 152,671評(píng)論 0 342
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)蝶怔。 經(jīng)常有香客問我奶浦,道長(zhǎng),這世上最難降的妖魔是什么踢星? 我笑而不...
    開封第一講書人閱讀 55,252評(píng)論 1 279
  • 正文 為了忘掉前任澳叉,我火速辦了婚禮,結(jié)果婚禮上沐悦,老公的妹妹穿的比我還像新娘成洗。我一直安慰自己,他們只是感情好藏否,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,253評(píng)論 5 371
  • 文/花漫 我一把揭開白布瓶殃。 她就那樣靜靜地躺著,像睡著了一般副签。 火紅的嫁衣襯著肌膚如雪遥椿。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,031評(píng)論 1 285
  • 那天淆储,我揣著相機(jī)與錄音冠场,去河邊找鬼。 笑死本砰,一個(gè)胖子當(dāng)著我的面吹牛碴裙,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播点额,決...
    沈念sama閱讀 38,340評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼舔株,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了还棱?” 一聲冷哼從身側(cè)響起载慈,我...
    開封第一講書人閱讀 36,973評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎诱贿,沒想到半個(gè)月后娃肿,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體咕缎,經(jīng)...
    沈念sama閱讀 43,466評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡珠十,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,937評(píng)論 2 323
  • 正文 我和宋清朗相戀三年料扰,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片焙蹭。...
    茶點(diǎn)故事閱讀 38,039評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡晒杈,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出孔厉,到底是詐尸還是另有隱情拯钻,我是刑警寧澤,帶...
    沈念sama閱讀 33,701評(píng)論 4 323
  • 正文 年R本政府宣布撰豺,位于F島的核電站粪般,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏污桦。R本人自食惡果不足惜亩歹,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,254評(píng)論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望凡橱。 院中可真熱鬧小作,春花似錦、人聲如沸稼钩。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,259評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)坝撑。三九已至静秆,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間巡李,已是汗流浹背抚笔。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評(píng)論 1 262
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留击儡,地道東北人塔沃。 一個(gè)月前我還...
    沈念sama閱讀 45,497評(píng)論 2 354
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像阳谍,于是被迫代替她去往敵國(guó)和親蛀柴。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,786評(píng)論 2 345