總結什麼的就不了,都太多了,不過很多都不太嚴謹或者不太準確,以當中的一些點提出說一下。只相信源碼。
- 桶數組長度取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。