關於hashmap不得不提

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

  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。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章