關於hashmap不得不提
總結什麼的就不了,都太多了,不過很多都不太嚴謹或者不太準確,以當中的一些點提出說一下。只相信原始碼。
- 桶陣列長度取2的次方的直接原因:將取模運算優化為做和length-1的與運算,並在此情況下,因為length-1二進位制位全為1,求index的結果會等同於hashcode的後n位,也就是可以認為,只要hashcode本身是均勻的,那麼hash演算法結果也是均勻的。 要點:不是為了減少碰撞把長度取為2的次方,而是如果要用與運算,一定要是2的n次方,如果是為了減少碰撞,那麼取素數才是最有效的。這裡主要是運算的優化
-
關於JDK1.7put頭插法,擴容頭插法,1.8put尾插法,擴容頭插法,也沒幾個說明白,部落格什麼的都好像差不多(你懂得),尾插法可以讓resize後連結串列不發生反轉,真的是這樣嗎?看過原始碼後,原來都在瞎xx說。(連結串列反轉不會對連結串列產生任何影響)
-
1)連結串列的反轉問題,和頭插法尾插法無關
-
先說擴容:1.7擴容,對原來的連結串列從第一個節點開始取,這裡以a->b->c->null為例子,不管1.7,1.8取節點都是從頭開始取往後遍歷,這個是不會變的,那麼1.7的操作是,取出a,做rehash,頭插到新陣列,這時新陣列為a->null,接下來取b頭插到新陣列,假設abc都rehash後還是同樣的index,那麼變成b->a->null,取c變成c->b->a->null,所以1.7中每一次擴容都會發生連結串列反轉,看原始碼
//JDK1.7 void transfer(Entry[] newTable, boolean rehash) { int newCapacity = newTable.length; //for迴圈中的程式碼,逐個遍歷連結串列,重新計算索引位置,將老陣列資料複製到新陣列中去 for (Entry<K,V> e : table) { while(null != e) { Entry<K,V> next = e.next; if (rehash) { e.hash = null == e.key ? 0 : hash(e.key); } int i = indexFor(e.hash, newCapacity); //將當前取到的entry的next鏈指向新的索引位置,newTable[i]有可能為空,有可能也是個entry鏈,如果是entry鏈,直接在連結串列頭部插入。 //關鍵的程式碼就是下面三行 e.next = newTable[i]; newTable[i] = e; e = next; } } }
-
-
而在1.8中,因為擴容後原連結串列上的Node可能會分成兩部分,通過用了兩條新連結串列:一條loHead下標為index,一條hiHead下標為index+Oldcap,通過遍歷所有的原連結串列中的節點,同樣a->b->c->null,這裡假設b的index和ac不同,那麼首先a,通過1.8的巧運算(遺棄了rehash,後面說)變成loHead->a->null,取b後變成hiHead->b->null,取c後變成loHead->a->c->null,這時遍歷完成,會將兩條新連結串列接到新陣列對應的index上,然後把新put的Node通過頭插插到連結串列頭,可以分析,1.8中就算put用的還是頭插法,resize後連結串列仍然不發生反轉。原始碼看關鍵部分40行到69行
//JDK1.8
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
if (oldCap > 0) {
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
else { // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
//這裡開始,把節點巧運算後分到兩條連結串列
else { // preserve order
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
//直到原連結串列全部Node取完,分別把兩條連結串列放到新陣列
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
//-------------------------------------------------
}
}
}
}
}
return newTab;
}
同時,因為這一部分(取出了40-60行),兩個連結串列依次在末端新增節點,在多執行緒下,第二個執行緒無非重複第一個執行緒一模一樣的操作,解決了多執行緒Put導致的死迴圈。但仍然不安全。
// preserve order
Node loHead = null, loTail = null;
Node hiHead = null, hiTail = null;
Node next;
//處理某個hash桶部分
do {
next = e.next;
{//確定在newTable中的位置
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
- 2)1.8中put用尾插法,猜想是因為紅黑樹,試想你往一棵樹里加節點,你會把他放在原來root根節點的位置還是放到樹的子節點呢?
-
3)關於1.8中捨棄rehash使用的巧運算。既對hash在新增的bit位看為0還是為1,詳細的這裡不說了,1.7的rehash是對擴容後的length做length-1的與操作,1.8是對e.hash & oldCap這樣的與操作
- 並沒有提升量級時間效能!不過減少了程式碼量。也減少了運算(原來rehash肯定步驟多)。
- 還有的說1.8對一個位的bit取與操作,讓原一個連結串列的節點均勻分為0或1,這是1.8的優化,我說大哥,能不能想一想,1.8的操作和1.7對length-1與操作的結果,有任何改變嗎,一模一樣的好嗎,1.8的運算就是對& length-1的一個轉換方式罷了。原來是1.7是a c+b c,現在寫為(a + b) * c,結果變沒變? 我看來,不過是寫JDK的人取了個巧罷了。
- 4)關於1.7中PUT用頭插法,因為插入連結串列的時候已經遍歷了一遍連結串列了,並不是說頭插比尾插更效率,只要插入都要摸鏈,那麼既然都摸到連結串列尾了,還使用頭插?這裡想想作業系統的某些排程演算法,是不是有一種,剛用過的資料極大可能馬上再用?【最近最久未使用】。這是時間區域性性原理。
最後,如果有誤,希望能指出,這裡是一個還沒學到java多執行緒的noob。
相關文章
- PHP 不得不提的 session 與 cookiePHPSessionCookie
- 不得不提的前端效能優化前端優化
- MySQL中不得不提的事務處理MySql
- 關於 Android studio 在xml中不提示的問題AndroidXML
- 關於React Hooks,你不得不知的事ReactHook
- HashTable、ConcurrentHashMap、TreeMap、HashMap關於鍵值的區別HashMap
- 關於HashMap的key重寫hashcode和equals的理解HashMap
- 關於Python程式語言不得不說的優缺點!Python
- 關於程式碼評審(CodeReview)那些不得不說的事兒View
- 最新研究進展:關於機器翻譯領域,這4個要點不得不關注
- 關於手機裡的IP地址,你不得不知道的“祕密”
- 微軟允許OEM對Win10不提供關閉Secure Boot微軟Win10boot
- 畢玄:我在阿里這十年,關於開源不得不說的事阿里
- 關於執行緒池你不得不知道的一些設定執行緒
- 關於 PHP 序列化和反序列化不得不知道的細節PHP
- Java中,那些關於String和字串常量池你不得不知道的東西Java字串
- 關於Java你不知道的那些事之Java8新特性[HashMap優化]JavaHashMap優化
- 關於雲端儲存網盤,企業不得不考慮的四大要素
- 【演算法】HashMap相關要點記錄演算法HashMap
- Hashtable/HashMap與key/value為null的關係HashMapNull
- Android 你不得不學的HTTP相關知識AndroidHTTP
- 有關 HashMap 面試會問的一切HashMap面試
- 關於 PHP 序列化和反序列化不得不知道的細節 (待確認)PHP
- 【面試精選】關於大型網站系統架構你不得不懂的10個問題面試網站架構
- 關於IT,關於技術
- win10程式執行不提示如何操作_win10電腦怎麼允許程式執行不提示Win10
- MySQL:begin後事務為什麼不提交MySql
- hashMap探析HashMap
- HashMap原理HashMap
- HashMap底層實現原理/HashMap與HashTable區別/HashMap與HashSet區別HashMap
- 關於
- 關於~
- 關於++[[]][+[]]+[+[]]
- 你不得不瞭解 Helm 3 中的 5 個關鍵新特性
- 原始碼分析系列1:HashMap原始碼分析(基於JDK1.8)原始碼HashMapJDK
- HashSet和HashMapHashMap
- Java集合:HashMapJavaHashMap
- 深度解析HashMapHashMap