HashTable畦粮,HashMap與ConcurrentHashMap源碼分析

HashMap與HashTable是兩個(gè)頗為相似的類(lèi)。抽象的說(shuō)乖阵,都是鍵值對(duì)集合宣赔,那么它們之前到達(dá)有什么區(qū)別呢?似乎面試也车山考啊儒将,我們從原理的角度來(lái)分析一下。

我們先來(lái)看看HashTable的代碼:

HashTable

首先对蒲,HashTable的核心是一個(gè)鍵值對(duì)數(shù)組钩蚊。如下:

HashtableEntry<K, V>[] table;

而鍵值對(duì)HashtableEntry的代碼我們可以看到:

    private static class HashtableEntry<K, V> implements Entry<K, V> {
        final K key;
        V value;
        final int hash;
        HashtableEntry<K, V> next;

        HashtableEntry(K key, V value, int hash, HashtableEntry<K, V> next) {
            this.key = key;
            this.value = value;
            this.hash = hash;
            this.next = next;
        }

        public final K getKey() {
            return key;
        }

        public final V getValue() {
            return value;
        }

        public final V setValue(V value) {
            if (value == null) {
                throw new NullPointerException("value == null");
            }
            V oldValue = this.value;
            this.value = value;
            return oldValue;
        }

        @Override public final boolean equals(Object o) {
            if (!(o instanceof Entry)) {
                return false;
            }
            Entry<?, ?> e = (Entry<?, ?>) o;
            return key.equals(e.getKey()) && value.equals(e.getValue());
        }

        @Override public final int hashCode() {
            return key.hashCode() ^ value.hashCode();
        }

        @Override public final String toString() {
            return key + "=" + value;
        }
    }

HashtablEntry是一個(gè)鏈表贡翘。它持有了一個(gè)鍵值對(duì)、一個(gè)hash值和鏈表中下一個(gè)元素两疚。那么為什么HashTable的鍵值對(duì)需要一個(gè)鏈表呢床估,鍵值對(duì)只有一個(gè)key和value不是就可以了嗎?為了回答上面的問(wèn)題诱渤,我們需要再來(lái)看看HashTable中put(K , V)方法的實(shí)現(xiàn):

   public synchronized V put(K key, V value) {
       if (key == null) {
           throw new NullPointerException("key == null");
       } else if (value == null) {
           throw new NullPointerException("value == null");
       }
       int hash = Collections.secondaryHash(key);
       HashtableEntry<K, V>[] tab = table;
       int index = hash & (tab.length - 1);
       HashtableEntry<K, V> first = tab[index];
       for (HashtableEntry<K, V> e = first; e != null; e = e.next) {
           if (e.hash == hash && key.equals(e.key)) {
               V oldValue = e.value;
               e.value = value;
               return oldValue;
           }
       }

       // No entry for key is present; create one
       modCount++;
       if (size++ > threshold) {
           rehash();  // Does nothing!!
           tab = doubleCapacity();
           index = hash & (tab.length - 1);
           first = tab[index];
       }
       tab[index] = new HashtableEntry<K, V>(key, value, hash, first);
       return null;
   }

上面的代碼我們首先要注意到synchronized丐巫,這個(gè)說(shuō)明HashTable很大可能是線程安全的,進(jìn)入方法后我們可以看到勺美,需要put一對(duì)鍵值對(duì)進(jìn)來(lái)時(shí)递胧,我們首先會(huì)從table中取出相應(yīng)hash位置的鍵值對(duì),然后進(jìn)行一個(gè)循環(huán)赡茸,如果hash和key均相等缎脾,則替換掉現(xiàn)有的值,直接return了占卧。

那么這里就有一種意外的情況遗菠,是不是有可能hash相等,key不相等华蜒。即哈希沖突辙纬。

在這樣的情況下,我們發(fā)現(xiàn)HashTable選擇將所有hash相等的鍵值對(duì)叭喜,存成一個(gè)鏈表贺拣。這樣,每次我們既可以用hash值捂蕴,統(tǒng)一索引位置譬涡,又可以避免hash沖突。

剩下的代碼就是擴(kuò)容啥辨、創(chuàng)建新的鍵值對(duì)涡匀,然后將新的鍵值對(duì)存入table。這里還會(huì)將和它hash值相同的鏈表溉知,放在它的尾部渊跋。

我們?cè)倏纯磄et(Object key)方法,與我們上面描述的原理相對(duì)應(yīng)着倾。

    public synchronized V get(Object key) {
        int hash = Collections.secondaryHash(key);
        HashtableEntry<K, V>[] tab = table;
        for (HashtableEntry<K, V> e = tab[hash & (tab.length - 1)];
                e != null; e = e.next) {
            K eKey = e.key;
            if (eKey == key || (e.hash == hash && key.equals(eKey))) {
                return e.value;
            }
        }
        return null;
    }

每次HashTable并不能直接在table中取得正確的值。取到的只是一個(gè)hash能夠?qū)?yīng)上的鏈表燕少。然后卡者,在這個(gè)鏈表中進(jìn)行遍歷,取到正確的值客们。這個(gè)方法叫“拉鏈法”崇决。


HashMap

HashMapEntry的代碼與HashTableEntry的代碼幾乎完全一樣材诽。因此,我們直接看put和get方法恒傻。

    @Override public V put(K key, V value) {
        if (key == null) {
            return putValueForNullKey(value);
        }

        int hash = Collections.secondaryHash(key);
        HashMapEntry<K, V>[] tab = table;
        int index = hash & (tab.length - 1);
        for (HashMapEntry<K, V> e = tab[index]; e != null; e = e.next) {
            if (e.hash == hash && key.equals(e.key)) {
                preModify(e);
                V oldValue = e.value;
                e.value = value;
                return oldValue;
            }
        }

        // No entry for (non-null) key is present; create one
        modCount++;
        if (size++ > threshold) {
            tab = doubleCapacity();
            index = hash & (tab.length - 1);
        }
        addNewEntry(key, value, hash, index);
        return null;
    }

我們發(fā)現(xiàn)put的方法也非常接近脸侥。只有一些細(xì)微的不同:

  1. HashMap的put方法沒(méi)有加鎖,因此HashMap并非線程安全的盈厘。
  2. HashMap的put方法睁枕,允許key和value為空。
  3. HashTable中有一個(gè)contains方法很容易引起誤解沸手,已在HashMap中取消外遇。
    public boolean contains(Object value) {
        return containsValue(value);
    }

ConcurrentHashMap

我們直接看ConcurrentHashMap的put方法:

    public V put(K key, V value) {
        return putVal(key, value, false);
    }

    /** Implementation for put and putIfAbsent */
    final V putVal(K key, V value, boolean onlyIfAbsent) {
        if (key == null || value == null) throw new NullPointerException();
        int hash = spread(key.hashCode());
        int binCount = 0;
        for (Node<K,V>[] tab = table;;) {
            Node<K,V> f; int n, i, fh;
            if (tab == null || (n = tab.length) == 0)
                tab = initTable();
            else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
                if (casTabAt(tab, i, null,
                             new Node<K,V>(hash, key, value, null)))
                    break;                   // no lock when adding to empty bin
            }
            else if ((fh = f.hash) == MOVED)
                tab = helpTransfer(tab, f);
            else {
            ...
        }
        addCount(1L, binCount);

由于代碼比較長(zhǎng),我們來(lái)逐段分析契吉。其中Node的結(jié)構(gòu)和我們前面所說(shuō)的HashMapEntry非常類(lèi)似是一個(gè)鏈表的節(jié)點(diǎn)跳仿。table也與前面一樣是個(gè)Node的數(shù)組。代碼首先就進(jìn)入了死循環(huán)捐晶,獲取了table對(duì)象菲语,如果table為空則會(huì)創(chuàng)建一個(gè)table,如果hash值所在的索引為空(即之前沒(méi)有相同的key存放在Map中)惑灵,則直接通過(guò)CAS原子操作賦值并退出循環(huán)山上。

接下來(lái)我們看看,如果ConcurrentHashMap中已經(jīng)存在了相同的key泣棋,ConcurrentHashMap是如何工作的胶哲。

                V oldVal = null;
                synchronized (f) {
                    if (tabAt(tab, i) == f) {
                        if (fh >= 0) {
                            binCount = 1;
                            for (Node<K,V> e = f;; ++binCount) {
                                K ek;
                                if (e.hash == hash &&
                                    ((ek = e.key) == key ||
                                     (ek != null && key.equals(ek)))) {
                                    oldVal = e.val;
                                    if (!onlyIfAbsent)
                                        e.val = value;
                                    break;
                                }
                                Node<K,V> pred = e;
                                if ((e = e.next) == null) {
                                    pred.next = new Node<K,V>(hash, key,
                                                              value, null);
                                    break;
                                }
                            }
                        }
                        else if (f instanceof TreeBin) {
                            Node<K,V> p;
                            binCount = 2;
                            if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
                                                           value)) != null) {
                                oldVal = p.val;
                                if (!onlyIfAbsent)
                                    p.val = value;
                            }
                        }
                    }
                }
                if (binCount != 0) {
                    if (binCount >= TREEIFY_THRESHOLD)
                        treeifyBin(tab, i);
                    if (oldVal != null)
                        return oldVal;
                    break;
                }

這段代碼首先鎖定了Node<K,V> f。并重新判斷了f有沒(méi)有被改變潭辈,如果已經(jīng)發(fā)生改變程序會(huì)跑出同步鎖鸯屿,binCount為0,會(huì)繼續(xù)循環(huán)把敢。如果沒(méi)有改變寄摆,且f的hash值不小于0,則binCount = 1修赞。此時(shí)已經(jīng)決定死循環(huán)一定會(huì)退出婶恼。此時(shí)再進(jìn)入新的循環(huán),和HashMap非常類(lèi)似柏副,通過(guò)拉鏈法防哈希沖突勾邦。

接下來(lái)我們?cè)賮?lái)看看ConcurrentHashMap的get方法:

    public V get(Object key) {
        Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
        int h = spread(key.hashCode());
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (e = tabAt(tab, (n - 1) & h)) != null) {
            if ((eh = e.hash) == h) {
                if ((ek = e.key) == key || (ek != null && key.equals(ek)))
                    return e.val;
            }
            else if (eh < 0)
                return (p = e.find(h, key)) != null ? p.val : null;
            while ((e = e.next) != null) {
                if (e.hash == h &&
                    ((ek = e.key) == key || (ek != null && key.equals(ek))))
                    return e.val;
            }
        }
        return null;
    }

由于在并發(fā)中,讀并不是很受影響割择。所以ConcurrentHashMap的get方法與HashMap的get方法比較相似眷篇。如果hash和key均相等則直接取出,只是hash相等則需要在鏈表中遍歷尋找key相等的對(duì)象荔泳。


通過(guò)分析三個(gè)類(lèi)的源碼分析蕉饼,我們嘗試總結(jié)一些結(jié)論:

共同點(diǎn):
  • 通過(guò)拉鏈法防哈希碰撞
HashTable:
  • 通過(guò)synchronized關(guān)鍵詞鎖定方法虐杯,保證線程安全。

  • key和value均不能為空

  • 有一個(gè)容易被誤解的contains昧港,其實(shí)就是containsValue

HashMap:
  • 不保證線程安全

  • key和value均可以為空

  • 不再有contains方法

ConcurrentHashMap:
  • 通過(guò)死循環(huán)檢測(cè)值是否相等擎椰,然后CAS的方式保證線程安全。只有在確認(rèn)需要更換已有對(duì)象時(shí)才會(huì)加鎖

  • key和value均不能為空


以上创肥。
水平有限达舒,望斧正。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末瓤的,一起剝皮案震驚了整個(gè)濱河市休弃,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌圈膏,老刑警劉巖塔猾,帶你破解...
    沈念sama閱讀 212,718評(píng)論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異稽坤,居然都是意外死亡丈甸,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,683評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門(mén)尿褪,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)睦擂,“玉大人,你說(shuō)我怎么就攤上這事杖玲《俪穑” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 158,207評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵摆马,是天一觀的道長(zhǎng)臼闻。 經(jīng)常有香客問(wèn)我,道長(zhǎng)囤采,這世上最難降的妖魔是什么述呐? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 56,755評(píng)論 1 284
  • 正文 為了忘掉前任,我火速辦了婚禮蕉毯,結(jié)果婚禮上乓搬,老公的妹妹穿的比我還像新娘。我一直安慰自己代虾,他們只是感情好进肯,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,862評(píng)論 6 386
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著棉磨,像睡著了一般坷澡。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 50,050評(píng)論 1 291
  • 那天频敛,我揣著相機(jī)與錄音,去河邊找鬼馅扣。 笑死斟赚,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的差油。 我是一名探鬼主播拗军,決...
    沈念sama閱讀 39,136評(píng)論 3 410
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼蓄喇!你這毒婦竟也來(lái)了发侵?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 37,882評(píng)論 0 268
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤妆偏,失蹤者是張志新(化名)和其女友劉穎刃鳄,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體钱骂,經(jīng)...
    沈念sama閱讀 44,330評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡叔锐,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,651評(píng)論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了见秽。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片愉烙。...
    茶點(diǎn)故事閱讀 38,789評(píng)論 1 341
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖解取,靈堂內(nèi)的尸體忽然破棺而出步责,到底是詐尸還是另有隱情,我是刑警寧澤禀苦,帶...
    沈念sama閱讀 34,477評(píng)論 4 333
  • 正文 年R本政府宣布蔓肯,位于F島的核電站,受9級(jí)特大地震影響伦忠,放射性物質(zhì)發(fā)生泄漏省核。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 40,135評(píng)論 3 317
  • 文/蒙蒙 一昆码、第九天 我趴在偏房一處隱蔽的房頂上張望气忠。 院中可真熱鬧,春花似錦赋咽、人聲如沸旧噪。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,864評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)淘钟。三九已至,卻和暖如春陪毡,著一層夾襖步出監(jiān)牢的瞬間米母,已是汗流浹背勾扭。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,099評(píng)論 1 267
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留铁瞒,地道東北人妙色。 一個(gè)月前我還...
    沈念sama閱讀 46,598評(píng)論 2 362
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像慧耍,于是被迫代替她去往敵國(guó)和親身辨。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,697評(píng)論 2 351

推薦閱讀更多精彩內(nèi)容

  • HashMap 是 Java 面試必考的知識(shí)點(diǎn)芍碧,面試官?gòu)倪@個(gè)小知識(shí)點(diǎn)就可以了解我們對(duì) Java 基礎(chǔ)的掌握程度煌珊。網(wǎng)...
    野狗子嗷嗷嗷閱讀 6,662評(píng)論 9 107
  • 前言 這次我和大家一起學(xué)習(xí)HashMap,HashMap我們?cè)诠ぷ髦薪?jīng)常會(huì)使用泌豆,而且面試中也很頻繁會(huì)問(wèn)到定庵,因?yàn)樗?..
    liangzzz閱讀 7,974評(píng)論 7 102
  • 天朗氣清,惠風(fēng)和暢践美,我吃掉一口空氣洗贰,給你寫(xiě)信。妒忌使無(wú)論卡爾維諾還是村上春樹(shù)都喪失行為能力陨倡,糟糕讓一團(tuán)亂麻頹然扎進(jìn)...
    飯團(tuán)小販閱讀 166評(píng)論 0 0
  • 最近因?yàn)樯磉叞l(fā)生了太多的事敛滋,心情也頗受影響。寫(xiě)下這些文字的時(shí)候兴革,剛剛得知了第三個(gè)噩耗绎晃,雖然這些事情并不是發(fā)生在我...
    上官云杰閱讀 149評(píng)論 0 0
  • 首都兒科研究所附屬兒童醫(yī)院到朝陽(yáng)公園南門(mén)的距離有多遠(yuǎn)?百度地圖上面顯示杂曲,出租車(chē)22分鐘庶艾,公交車(chē)53分鐘,自行車(chē)32...
    兔小寶呢閱讀 447評(píng)論 0 1