數據結構算法 - HashMap 源碼深度解析

  • equals 和 == 的區(qū)別姓蜂,hashCode 與它們之間的聯(lián)系胖喳?
  • HashMap 的長度為什么是 2 的冪次孵延?
  • 五個線程同時往 HashMap 中 put 數據會發(fā)生什么侵俗?
  • ConcurrentHashMap 是怎么保證線程安全的痛倚?

上面是一些常見的面試題,本文旨在分析 HashMap 的源碼實現(xiàn)思想耸峭,并不會去細講這些問題桩蓉,在我們看完源碼之后不妨自己做一些思考。本文也不會細講 JDK 1.8 的紅黑樹和分段鎖劳闹,這部分內容等我們分析完二叉樹之后院究,再來做一些鞏固分析。

我們現(xiàn)在不妨思考一下本涕,假設讓我們自己來設計一個類似 HashMap 的類业汰,我估計大部分能想到的是,用一個數組或者用一個 ArrayList 直接來存放一個 Entry 對象菩颖,Entry 對象存放 put 的 key 和 value 样漆。因為 HashMap 是不允許鍵值重復的,如果我們直接用數組來作為存儲結構位他,在不考慮數組擴容的情況下,其查詢和插入的復雜度都是 O(n) 級別的产场。

HashMap 采用數組 + 鏈表的實現(xiàn)就很好的優(yōu)化了我們上面所講的問題鹅髓,大致的原理是當我們 put 一個 key 和 value 時,我們首先會對 key 進行二次 hash 處理京景, 然后根據 hash 值找到其所在的數組的角標位置窿冯,再去遍歷鏈表判斷是否有 key 重復,如果有則覆蓋沒有則添加确徙。這種實現(xiàn)方式在理想的情況下醒串,查詢和添加的時間復雜度是 O(1)执桌,請看下面這張圖(長度應該是 2 的冪次):

圖片來源于網絡

上面有一步操作是獲取 key 的 hashCode() , 然后進行二次 hash ,那么 hashCode() 到底返回的是什么呢芜赌?我們不妨來看看源碼:

    public int hashCode() {
        return identityHashCode(this);
    }

    // Android-changed: add a local helper for identityHashCode.
    // Package-private to be used by j.l.System. We do the implementation here
    // to avoid Object.hashCode doing a clinit check on j.l.System, and also
    // to avoid leaking shadow$_monitor_ outside of this class.
    /* package-private */ static int identityHashCode(Object obj) {
        int lockWord = obj.shadow$_monitor_;
        final int lockWordStateMask = 0xC0000000;  // Top 2 bits.
        final int lockWordStateHash = 0x80000000;  // Top 2 bits are value 2 (kStateHash).
        final int lockWordHashMask = 0x0FFFFFFF;  // Low 28 bits.
        if ((lockWord & lockWordStateMask) == lockWordStateHash) {
            return lockWord & lockWordHashMask;
        }
        return identityHashCodeNative(obj);
    }

    @FastNative
    private static native int identityHashCodeNative(Object obj);

hashCode 最終調用的是 identityHashCodeNative 的 native 方法仰挣,之前學的 NDK 現(xiàn)在就可以派上用場了,我們不妨跟蹤到 JNI 層去看看里面具體的實現(xiàn)缠沈,目錄在 jdk\src\share\native\java\lang\Object.c 我們挑一些關鍵代碼:

markOop mark = ReadStableMark (obj);
// 如果當前對象沒有鎖
if (mark->is_neutral()) {
    hash = mark->hash();              // this is a normal header
    if (hash) {                       // if it has hash, just return it
      return hash;
    }
    hash = get_next_hash(Self, obj);  // allocate a new hash code
    temp = mark->copy_set_hash(hash); // merge the hash code into header
    // use (machine word version) atomic operation to install the hash
    test = (markOop) Atomic::cmpxchg_ptr(temp, obj->mark_addr(), mark);
    if (test == mark) {
      return hash;
    }
    // If atomic operation failed, we must inflate the header
    // into heavy weight monitor. We could add more code here
    // for fast path, but it does not worth the complexity.
  } else if (mark->has_monitor()) {
    monitor = mark->monitor();
    temp = monitor->header();
    assert (temp->is_neutral(), "invariant") ;
    hash = temp->hash();
    if (hash) {
      return hash;
    }
    // Skip to the following code to reduce code size
  } else if (Self->is_lock_owned((address)mark->locker())) {
    //如果重入
    temp = mark->displaced_mark_helper(); // this is a lightweight monitor owned
    assert (temp->is_neutral(), "invariant") ;
    hash = temp->hash();              // by current thread, check if the displaced
    if (hash) {                       // header contains hash code
      return hash;
    }
    // WARNING:
    //   The displaced header is strictly immutable.
    // It can NOT be changed in ANY cases. So we have
    // to inflate the header into heavyweight monitor
    // even the current thread owns the lock. The reason
    // is the BasicLock (stack slot) will be asynchronously
    // read by other threads during the inflate() function.
    // Any change to stack may not propagate to other threads
    // correctly.
  }

  // Inflate the monitor to set hash code
  monitor = ObjectSynchronizer::inflate(Self, obj);
  // Load displaced header and check it has hash code
  mark = monitor->header();
  assert (mark->is_neutral(), "invariant") ;
  hash = mark->hash();
  if (hash == 0) {
    hash = get_next_hash(Self, obj);
    temp = mark->copy_set_hash(hash); // merge hash code into header
    assert (temp->is_neutral(), "invariant") ;
    test = (markOop) Atomic::cmpxchg_ptr(temp, monitor, mark);
    if (test != mark) {
      // The only update to the header in the monitor (outside GC)
      // is install the hash code. If someone add new usage of
      // displaced header, please update this code
      hash = test->hash();
      assert (test->is_neutral(), "invariant") ;
      assert (hash != 0, "Trivial unexpected object/monitor header usage.");
    }
  }
  // We finally get the hash
  return hash;

  // hash operations
  intptr_t hash() const {
    // value() 是地址
    // age_bits                 = 4,
    // cms_shift                = age_shift + age_bits,
    // hash_shift               = cms_shift + cms_bits,
    // if 64 位
    // hash_mask = right_n_bits(hash_bits);
    return mask_bits(value() >> hash_shift, hash_mask);
  }

看不懂先不必去深究膘壶,根據源碼我們就能發(fā)現(xiàn),hashCode() 返回的并不是地址那么簡單洲愤,而是經過了一系列的計算得到的颓芭,有兩個概念我們需要了解:兩個不同的對象 hashCode 值可能會相等,hashCode 值不相等的兩個對象肯定不是同一對象柬赐。

簡單分析最后兩行關鍵代碼亡问,在 C++ 中兩個不同的對象的地址肯定是不同的,但是 value() >> 16 就有可能相等肛宋,源碼原理就這么簡單州藕。接下來我們回到我們的重點去分析 HashMap 的源碼(JDK 1.7)

    // 確保數組 tab 的長度是 2 的冪次,上次已講悼吱,不再解釋
    static final int tableSizeFor(int cap) {
        int n = cap - 1;
        n |= n >>> 1;
        n |= n >>> 2;
        n |= n >>> 4;
        n |= n >>> 8;
        n |= n >>> 16;
        return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
    }

    public V put(K key, V value) {
        // 如果是空的創(chuàng)建一個新的數組
        if (table == EMPTY_TABLE) {
            inflateTable(threshold);
        }
        // 如果 key 是 null 慎框,單獨存放
        if (key == null)
            return putForNullKey(value);
        // 二次 hash 獲取 hash 值
        int hash = hash(key);
        // 獲取應當存放的 tab 位置
        int i = indexFor(hash, table.length);
        // 遍歷 i 上面的鏈表看有沒有存在,如果有覆蓋
        for (HashMapEntry<K,V> e = table[i]; e != null; e = e.next) {
            Object k;
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }
        // key 不存在添加新的
        modCount++;
        addEntry(hash, key, value, i);
        return null;
    }
    
    void addEntry(int hash, K key, V value, int bucketIndex) {
        // 是否需要擴容
        if ((size >= threshold) && (null != table[bucketIndex])) {
            resize(2 * table.length);
            hash = (null != key) ? hash(key) : 0;
            bucketIndex = indexFor(hash, table.length);
        }
        // 創(chuàng)建新的節(jié)點
        createEntry(hash, key, value, bucketIndex);
    }
    
    void resize(int newCapacity) {
        HashMapEntry[] oldTable = table;
        int oldCapacity = oldTable.length;
        if (oldCapacity == MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return;
        }

        HashMapEntry[] newTable = new HashMapEntry[newCapacity];
        transfer(newTable);
        table = newTable;
        threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
    }    
    
    // 擴容需要重新擺放里面的所有元素(最耗時的一個操作)
    void transfer(HashMapEntry[] newTable) {
        int newCapacity = newTable.length;
        for (HashMapEntry<K,V> e : table) {
            while(null != e) {
                HashMapEntry<K,V> next = e.next;
                int i = indexFor(e.hash, newCapacity);
                e.next = newTable[i];
                newTable[i] = e;
                e = next;
            }
        }
    }

    void createEntry(int hash, K key, V value, int bucketIndex) {
        // 添加在列表最前面
        HashMapEntry<K,V> e = table[bucketIndex];
        table[bucketIndex] = new HashMapEntry<>(hash, key, value, e);
        size++;
    }
    
    
    final Entry<K,V> getEntry(Object key) {
        if (size == 0) {
            return null;
        }

        int hash = (key == null) ? 0 : hash(key);
        // 獲取所在的位置后添,遍歷循環(huán)返回
        for (HashMapEntry<K,V> e = table[indexFor(hash, table.length)];
             e != null;
             e = e.next) {
            Object k;
            if (e.hash == hash &&
                ((k = e.key) == key || (key != null && key.equals(k))))
                return e;
        }
        return null;
    }

了解了思想笨枯,源碼是非常容易理解的,至于 JDK 1.8 的紅黑樹知識遇西,我們必須先要了解二叉樹馅精;ConcurrentHashMap 分段鎖我們都會在后面的文章陸陸續(xù)續(xù)的做一些分析。

視頻鏈接:https://pan.baidu.com/s/1dYd-4UG0VY1UhezE1p9s9A
視頻密碼:b8zd

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末粱檀,一起剝皮案震驚了整個濱河市洲敢,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌茄蚯,老刑警劉巖压彭,帶你破解...
    沈念sama閱讀 206,482評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異渗常,居然都是意外死亡壮不,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,377評論 2 382
  • 文/潘曉璐 我一進店門皱碘,熙熙樓的掌柜王于貴愁眉苦臉地迎上來询一,“玉大人,你說我怎么就攤上這事〗∪铮” “怎么了菱阵?”我有些...
    開封第一講書人閱讀 152,762評論 0 342
  • 文/不壞的土叔 我叫張陵,是天一觀的道長缩功。 經常有香客問我晴及,道長,這世上最難降的妖魔是什么掂之? 我笑而不...
    開封第一講書人閱讀 55,273評論 1 279
  • 正文 為了忘掉前任抗俄,我火速辦了婚禮,結果婚禮上世舰,老公的妹妹穿的比我還像新娘动雹。我一直安慰自己,他們只是感情好跟压,可當我...
    茶點故事閱讀 64,289評論 5 373
  • 文/花漫 我一把揭開白布胰蝠。 她就那樣靜靜地躺著,像睡著了一般震蒋。 火紅的嫁衣襯著肌膚如雪茸塞。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,046評論 1 285
  • 那天查剖,我揣著相機與錄音钾虐,去河邊找鬼。 笑死笋庄,一個胖子當著我的面吹牛效扫,可吹牛的內容都是我干的。 我是一名探鬼主播直砂,決...
    沈念sama閱讀 38,351評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼菌仁,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了静暂?” 一聲冷哼從身側響起济丘,我...
    開封第一講書人閱讀 36,988評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎洽蛀,沒想到半個月后摹迷,有當地人在樹林里發(fā)現(xiàn)了一具尸體,經...
    沈念sama閱讀 43,476評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡郊供,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 35,948評論 2 324
  • 正文 我和宋清朗相戀三年峡碉,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片颂碘。...
    茶點故事閱讀 38,064評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡异赫,死狀恐怖,靈堂內的尸體忽然破棺而出头岔,到底是詐尸還是另有隱情塔拳,我是刑警寧澤,帶...
    沈念sama閱讀 33,712評論 4 323
  • 正文 年R本政府宣布峡竣,位于F島的核電站靠抑,受9級特大地震影響,放射性物質發(fā)生泄漏适掰。R本人自食惡果不足惜颂碧,卻給世界環(huán)境...
    茶點故事閱讀 39,261評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望类浪。 院中可真熱鬧载城,春花似錦、人聲如沸费就。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,264評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽力细。三九已至睬澡,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間眠蚂,已是汗流浹背煞聪。 一陣腳步聲響...
    開封第一講書人閱讀 31,486評論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留逝慧,地道東北人昔脯。 一個月前我還...
    沈念sama閱讀 45,511評論 2 354
  • 正文 我出身青樓,卻偏偏與公主長得像馋艺,于是被迫代替她去往敵國和親栅干。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 42,802評論 2 345