關於hashmap不得不提

jdk1.80發表於2018-09-20

總結什麼的就不了,都太多了,不過很多都不太嚴謹或者不太準確,以當中的一些點提出說一下。只相信原始碼。

  1. 桶陣列長度取2的次方的直接原因:將取模運算優化為做和length-1的與運算,並在此情況下,因為length-1二進位制位全為1,求index的結果會等同於hashcode的後n位,也就是可以認為,只要hashcode本身是均勻的,那麼hash演算法結果也是均勻的。 要點:不是為了減少碰撞把長度取為2的次方,而是如果要用與運算,一定要是2的n次方,如果是為了減少碰撞,那麼取素數才是最有效的。這裡主要是運算的優化
  2. 關於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。


相關文章