Jdk1.7下的HashMap源碼分析

本文主要討論jdk1.7下hashMap的源碼實現,其中主要是在擴容時容易出現死循環的問題,以及put元素的整個過程。java

一、數組結構

數組+鏈表

示例圖以下:算法

常量屬性數組

/**
 * The default initial capacity - MUST be a power of two.
 * 默認初始容量大小
 */
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

/**
 * MUST be a power of two <= 1<<30.
 * hashMap最大容量,可裝元素個數
 */
static final int MAXIMUM_CAPACITY = 1 << 30;

/**
 * The load factor used when none specified in constructor.
 * 加載因子,如容量爲16,默認閾值即爲16*0.75=12,元素個數超過(包含)12且,擴容
 */
static final float DEFAULT_LOAD_FACTOR = 0.75f;
/**
 * 空數組,默認數組爲空,初始化後才纔有內存地址,第一次put元素時判斷,延遲初始化
 */
static final Entry<?,?>[] EMPTY_TABLE = {};

二、存在的死循環問題

擴容致使的死循環,jdk1.7中在多線程高併發環境容易出死循環,致使cpu使用率太高問題,問題出在擴容方法resize()中,更具體內部的transfer方法:將舊數組元素轉移到新數組過程當中,源碼以下:安全

void resize(int newCapacity) {
    Entry[] oldTable = table;
    int oldCapacity = oldTable.length;
 //1.若是原來數組容量等於最大值了,2^30,設置擴容閾值爲Integer最大值,不須要再擴容
    if (oldCapacity == MAXIMUM_CAPACITY) {
        threshold = Integer.MAX_VALUE;
        return;
    }
  //2.建立新數組對象 
    Entry[] newTable = new Entry[newCapacity];
 //3.將舊數組元素轉移到新數組中,分析一
    transfer(newTable, initHashSeedAsNeeded(newCapacity));
 //4.從新引用新數組對象和計算新的閾值
    table = newTable;
    threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}

transfer方法多線程

/**
 * Transfers all entries from current table to newTable.
 * 從當前數組中轉移全部的節點到新數組中
 */
void transfer(Entry[] newTable, boolean rehash) {
    int newCapacity = newTable.length;
    //遍歷舊數組
    for (Entry<K,V> e : table) {
    //1,首先獲取數組下標元素
        while(null != e) {
            //2.獲取數組該桶位置鏈表中下一個元素
            Entry<K,V> next = e.next;
     //3.是否須要從新該元素key的hash值
            if (rehash) {
                e.hash = null == e.key ? 0 : hash(e.key);
            }
    //4,從新肯定在新數組中下標位置
            int i = indexFor(e.hash, newCapacity);
    //5.頭插法:插入新鏈表該桶位置,如有元素,就造成鏈表,每次新加入的節點都插在第一位,就數組下標位置
             e.next = newTable[i];
            newTable[i] = e;
    //6.繼續獲取鏈表下一個元素        
            e = next;
        }
    }
}


//傳入容量值返回是否須要對key從新Hash
final boolean initHashSeedAsNeeded(int capacity) {
    //1.hashSeed默認爲0,所以currentAltHashing爲false
    boolean currentAltHashing = hashSeed != 0;
   //2,sun.misc.VM.isBooted()在類加載啓動成功後,狀態會修改成true
  // 所以變數在於,capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD,debug發現正常狀況ALTERNATIVE_HASHING_THRESHOLD是一個很大的值,使用的是Integer的最大值
    boolean useAltHashing = sun.misc.VM.isBooted() &&
            (capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
    //3,二者異或,只有不相同時才爲true,即useAltHashing =true時,dubug代碼發現useAltHashing =false,
    boolean switching = currentAltHashing ^ useAltHashing;
    if (switching) {
        hashSeed = useAltHashing
            ? sun.misc.Hashing.randomHashSeed(this)
            : 0;
    }
  //正常狀況下是返回false,即不須要從新對key哈希
    return switching;
}

上面源碼展現轉移元素過程:併發

如下模擬2個線程併發操做hashMap 在put元素時形成的死循環過程:app

鏈表死循環圖例:dom

三、put方法

1.7的put方法,因沒有紅黑樹結構,相比較1.8簡單, 容易理解,流程圖以下所示:函數

代碼以下:高併發

public V put(K key, V value) {
    //1,若當前數組爲空,初始化
    if (table == EMPTY_TABLE) {
       //分析1
        inflateTable(threshold);
    }
    //2,若put的key爲null,在放置在數組下標第一位,索引爲0位置,從該源碼可知
   // hashMap容許 鍵值對 key=null,可是隻能有惟一一個
    if (key == null)
        // 分析2
        return putForNullKey(value);
    //3,計算key的hash,這裏與1.8有區別 
    //分析3  
    int hash = hash(key);
    // 4,肯定在數組下標位置,與1.8相同
    int i = indexFor(hash, table.length);
    // 5,遍歷該數組位置,即該桶處遍歷
    for (Entry<K,V> e = table[i]; e != null; e = e.next) {
        Object k;
        if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
        // 找到相同的key,則覆蓋原value值,返回舊值
            V oldValue = e.value;
            e.value = value;
            //該方法爲空,不用看
            e.recordAccess(this);
            return oldValue;
        }
    }
   //由於hashMap線程不安全,修改操做沒有同步鎖,
   //該字段值用於記錄修改次數,用於快速失敗機制 fail-fast,防止其餘線程同時作了修改,拋出併發修改異常
    modCount++;
    // 6,原數組中沒有相同的key,以頭插法插入新的元素
    //分析4
    addEntry(hash, key, value, i);
    return null;
}

分析1: HashMap如何初始化數組的,延遲初始化有什麼好處?

結論: 一、1.7,1.8都是延遲初始化,在put第一個元素時建立數組,目的是爲了節省內存。

初始化代碼:

private void inflateTable(int toSize) {
    // Find a power of 2 >= toSize
    //1.該方法很是重要,目的爲了獲得一個比toSize最接近的2的冪次方的數,
   // 且該數要>=toSize,這個2的冪次方方便後面各類位運算
   // 如:new HashMap(15),指定15大小集合,內部實際 建立數組大小爲2^4=16
   // 分析見下
    int capacity = roundUpToPowerOf2(toSize);
   //2,肯定擴容閾值
    threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
    //3,初始化數組對象
    table = new Entry[capacity];
    initHashSeedAsNeeded(capacity);
}

Q:如何確保獲取到比toSize 最接近且大於等於它的2的冪次方的數?

深刻理解roundUpToPowerOf2方法:

private static int roundUpToPowerOf2(int number) {
    // assert number >= 0 : "number must be non-negative";
//若是number大於等於最大值 2^30,賦值爲最大,主要是防止傳參越界,number必定是否非負的 
    return number >= MAXIMUM_CAPACITY
            ? MAXIMUM_CAPACITY
        : (number > 1) ? Integer.highestOneBit((number - 1) << 1) : 1;
        //核心在於Integer.highestOneBit((number - 1) << 1) 此處
}

先拋出2個問題:

1:這個 (number - 1) << 1 的做用是什麼?

2:這個方法highestOneBit確定是爲了獲取到知足條件的2的冪次方的數,背後的原理呢?

結論: Integer的方法highestOneBit(i) 這個方法是經過位運算,獲取到i的二進制位最左邊(最高位)的1,其他位都抹去,置爲0,即獲取的是小於等於i的2的冪次方的數.

若是直接傳入number,那麼獲取到的是2的冪次方的數,可是該數必定小於等於number,但這不是咱們的目的;

如highestOneBit(15)=8highestOneBit(21)=16而咱們是想要獲取一個剛剛大於等於number的2次方的數,(number-1)<<1 所以須要先將number 擴大二倍number <<1 , 爲何須要number-1,是考慮到臨界值問題,剛好number自己就是2的冪次方,如 number=16,擴大2倍後爲32, highestOneBit方法計算後結果仍是32,這不符合需求。

public static int highestOneBit(int i) {
    // HD, Figure 3-1
    i |= (i >>  1);
    i |= (i >>  2);
    i |= (i >>  4);
    i |= (i >>  8);
    i |= (i >> 16);
    return i - (i >>> 1);
}

2的冪次方二進值特色:只有最高位爲1,其餘位全爲0

目的:將傳入i的二進制最左邊的1保留,其他低位的1全變爲0

原理:某數二進制: 0001 ,不關心其低位是什麼,以*代替,進行運算

  • 右移1位
i |= (i >> 1); 
 0001****
|
 00001***  
----------
 00011***  #保證左邊2位是1
  • 右移2位
i |= (i >> 2); 
 00011***
|
 0000011* 
----------
 0001111*  #保證左邊4位是1
  • 右移4位
i |= (i >> 4); 
 0001111*
|
 00000001 
----------
 00011111  #把高位如下全部位變爲1了,該數仍是隻有5位,該計算可將8位下全部的置爲1

Q:爲何要再執行右移8位,16位?

因int類型 4個字節,32位,這樣能夠必定能夠保證將低位全置爲1;

  • 最後一步,大功告成!
i - (i >>> 1);
#此時 i= 00011111
 00011111
-
 00001111 #無符號右移1位
---------
 00010000  #拿到值

分析2: HashMap如何處理key 爲null狀況,value呢?

結論:

  1. 容許key爲null,但最多惟一存在一個,放在數組下標爲0位置
  2. value爲null的鍵值對能夠有多個
  3. 由1,2 推得,鍵值對都爲null的Entry對象能夠有,但最多一個
private V putForNullKey(V value) {
    //1.直接table[0] 位置獲取,先遍歷鏈表(這裏對該數組位置統稱爲鏈表,可能沒有元素,或者只有一個元素,或者鏈表)查找是否存在相同的key,存在覆蓋原值 
    for (Entry<K,V> e = table[0]; e != null; e = e.next) {
        if (e.key == null) {
            V oldValue = e.value;
            e.value = value;
            e.recordAccess(this);
            return oldValue;
        }
    }
    modCount++;
  //此時注意添加節點時,第一個0即表明數組下標位置,後面會分析該方法
    addEntry(0, null, value, 0);
    return null;
}

分析3:如何實現hash算法,保證key的hash值均勻分散,減小hash衝突?

jdk1.7中爲了儘量的對key的hash後均勻分散,擾動函數實現採用了 5次異或+4次位移

final int hash(Object k) {
    int h = hashSeed;
    if (0 != h && k instanceof String) {
        return sun.misc.Hashing.stringHash32((String) k);
    }
    //k的hashCode值 與hashSeed 異或 
    h ^= k.hashCode();
    // This function ensures that hashCodes that differ only by
    // constant multiples at each bit position have a bounded
    // number of collisions (approximately 8 at default load factor).
    h ^= (h >>> 20) ^ (h >>> 12);
    return h ^ (h >>> 7) ^ (h >>> 4);
}

分析4:插入新的節點到map中,若是原數組總元素個數超過閾值,先擴容再插入節點

void addEntry(int hash, K key, V value, int bucketIndex) {
    //總元素個數大於等於閾值 且 當前數組下標已存在元素了: 擴容
    if ((size >= threshold) && (null != table[bucketIndex])) {
        //1,擴容,上面已分析過代碼
        resize(2 * table.length);
        //2,計算新加key的hash值,key爲null的hash值爲0
        hash = (null != key) ? hash(key) : 0;
        //3,確保計算的數組下標必定在數組有效索引內,見分析5
        bucketIndex = indexFor(hash, table.length);
    }
    // 4,擴容後再插入新數組中
    createEntry(hash, key, value, bucketIndex);
}
//分析5
static int indexFor(int h, int length) {
    // 與數組長度-1與運算,必定能夠確保結果值在數組有效索引內,且均勻分散
    return h & (length-1);
}
// 進一步分析插入節點方法
void createEntry(int hash, K key, V value, int bucketIndex) {
   //1,首先獲取新數組索引位置元素
    Entry<K,V> e = table[bucketIndex];
    //2,頭插法插入新節點, Entry構造方法第4個參數e表示指定當前新增節點的next指針指向該節點,造成鏈表
    table[bucketIndex] = new Entry<>(hash, key, value, e);
    //3,map元素個數+1
    size++;
}

參考:

1、1.7解析:http://www.javashuo.com/article/p-abgrqhmn-a.html

2、1.8解析:https://www.jianshu.com/p/8324a34577a0

相關文章
相關標籤/搜索