深刻理解HashMap(jdk7)

HashMap中的存儲結構圖示

​ jdk1.7的HashMap採用數組+單鏈表實現,儘管定義了hash函數來避免衝突,但由於數組長度有限,仍是會出現兩個不一樣的Key通過計算後在數組中的位置同樣,1.7版本中採用了鏈表來解決。java

​ 從上面的簡易示圖中也能發現,若是位於鏈表中的結點過多,那麼很顯然經過key值依次查找效率過低,因此在1.8中對其進行了改良,採用數組+鏈表+紅黑樹來實現,當鏈表長度超過閾值8時,將鏈表轉換爲紅黑樹.具體細節參考我上一篇總結的 深刻理解jdk8中的HashMap算法

​ 從上面圖中也知道實際上每一個元素都是Entry類型,因此下面再來看看Entry中有哪些屬性(在1.8中Entry更名爲Node,一樣實現了Map.Entry)。數組

//hash標中的結點Node,實現了Map.Entry
static class Entry<K,V> implements Map.Entry<K,V> {
    final K key;
    V value;
    Entry<K,V> next;
    int hash;
	//Entry構造器,須要key的hash,key,value和next指向的結點
    Entry(int h, K k, V v, Entry<K,V> n) {
        value = v;
        next = n;
        key = k;
        hash = h;
    }

    public final K getKey() { return key; }

    public final V getValue() { return value; }

    public final V setValue(V newValue) {
        V oldValue = value;
        value = newValue;
        return oldValue;
    }
    //equals方法
    public final boolean equals(Object o) {
        if (!(o instanceof Map.Entry))
            return false;
        Map.Entry e = (Map.Entry)o;
        Object k1 = getKey();
        Object k2 = e.getKey();
        if (k1 == k2 || (k1 != null && k1.equals(k2))) {
            Object v1 = getValue();
            Object v2 = e.getValue();
            if (v1 == v2 || (v1 != null && v1.equals(v2)))
                return true;
        }
        return false;
    }
	//重寫Object的hashCode
    public final int hashCode() {
        return Objects.hashCode(getKey()) ^ Objects.hashCode(getValue());
    }

    public final String toString() {
        return getKey() + "=" + getValue();
    }

	//調用put(k,v)方法時候,若是key相同即Entry數組中的值會被覆蓋,就會調用此方法。
    void recordAccess(HashMap<K,V> m) {
    }

    //只要從表中刪除entry,就會調用此方法
    void recordRemoval(HashMap<K,V> m) {
    }
}
複製代碼

HashMap中的成員變量以及含義

//默認初始化容量初始化=16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

//最大容量 = 1 << 30
static final int MAXIMUM_CAPACITY = 1 << 30;

//默認加載因子.通常HashMap的擴容的臨界點是當前HashMap的大小 > DEFAULT_LOAD_FACTOR * 
//DEFAULT_INITIAL_CAPACITY = 0.75F * 16
static final float DEFAULT_LOAD_FACTOR = 0.75f;

//默認是空的table數組
static final Entry<?,?>[] EMPTY_TABLE = {};

//table[]默認也是上面給的EMPTY_TABLE空數組,因此在使用put的時候必須resize長度爲2的冪次方值
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;

//map中的實際元素個數 != table.length
transient int size;

//擴容閾值,當size大於等於其值,會執行resize操做
//通常狀況下threshold=capacity*loadFactor
int threshold;

//hashTable的加載因子
final float loadFactor;

/** * The number of times this HashMap has been structurally modified * Structural modifications are those that change the number of mappings in * the HashMap or otherwise modify its internal structure (e.g., * rehash). This field is used to make iterators on Collection-views of * the HashMap fail-fast. (See ConcurrentModificationException). */
transient int modCount;

//hashSeed用於計算key的hash值,它與key的hashCode進行按位異或運算
//hashSeed是一個與實例相關的隨機值,用於解決hash衝突
//若是爲0則禁用備用哈希算法
transient int hashSeed = 0;
複製代碼

HashMap構造方法

​ 咱們看看HashMap源碼中爲咱們提供的四個構造方法。安全

//(1)無參構造器:
//構造一個空的table,其中初始化容量爲DEFAULT_INITIAL_CAPACITY=16。加載因子爲DEFAULT_LOAD_FACTOR=0.75F
public HashMap() {
    this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
}
複製代碼
//(2)指定初始化容量的構造器
//構造一個空的table,其中初始化容量爲傳入的參數initialCapacity。加載因子爲DEFAULT_LOAD_FACTOR=0.75F
public HashMap(int initialCapacity) {
    this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
複製代碼
//(3)指定初始化容量和加載因子的構造器
//構造一個空的table,初始化容量爲傳入參數initialCapacity,加載因子爲loadFactor
public HashMap(int initialCapacity, float loadFactor) {
    //對傳入初始化參數進行合法性檢驗,<0就拋出異常
    if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal initial capacity: " +
                                           initialCapacity);
    //若是initialCapacity大於最大容量,那麼容量=MAXIMUM_CAPACITY
    if (initialCapacity > MAXIMUM_CAPACITY)
        initialCapacity = MAXIMUM_CAPACITY;
    //對傳入加載因子參數進行合法檢驗,
    if (loadFactor <= 0 || Float.isNaN(loadFactor))
        //<0或者不是Float類型的數值,拋出異常
        throw new IllegalArgumentException("Illegal load factor: " +
                                           loadFactor);
	//兩個參數檢驗完了,就給本map實例的屬性賦值
    this.loadFactor = loadFactor;
    threshold = initialCapacity;
    //init是一個空的方法,模板方法,若是有子類須要擴展能夠自行實現
    init();
}
複製代碼

​ 從上面的這3個構造方法中咱們能夠發現雖然指定了初始化容量大小,但此時的table仍是空,是一個空數組,且擴容閾值threshold爲給定的容量或者默認容量(前兩個構造方法實際上都是經過調用第三個來完成的)。在其put操做前,會建立數組(跟jdk8中使用無參構造時候相似).多線程

//(4)參數爲一個map映射集合
//構造一個新的map映射,使用默認加載因子,容量爲參數map大小除以默認負載因子+1與默認容量的最大值
public HashMap(Map<? extends K, ? extends V> m) {
    //容量:map.size()/0.75+1 和 16二者中更大的一個
    this(Math.max(
        	(int) (m.size() / DEFAULT_LOAD_FACTOR) + 1,
                  DEFAULT_INITIAL_CAPACITY), 
         DEFAULT_LOAD_FACTOR);
    inflateTable(threshold);
    //把傳入的map裏的全部元素放入當前已構造的HashMap中
    putAllForCreate(m);
}
複製代碼

​ 這個構造方法即是在put操做前調用inflateTable方法,這個方法具體的做用就是建立一個新的table用之後面使用putAllForCreate裝入傳入的map中的元素,這個方法咱們來看下,注意剛也提到了此時的threshold擴容閾值是初始容量。下面對其中的一些方法進行說明併發

(1)inflateTable方法說明

​ 這個方法比較重要,在第四種構造器中調用了這個方法。而若是建立集合對象的時候使用的是前三種構造器的話會在調用put方法的時候調用該方法對table進行初始化app

private void inflateTable(int toSize) {
    //返回不小於number的最小的2的冪數,最大爲MAXIMUM_CAPACITY,類比jdk8的實現中的tabSizeFor的做用
    int capacity = roundUpToPowerOf2(toSize);
	//擴容閾值爲:(容量*加載因子)和(最大容量+1)中較小的一個
    threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
    //建立table數組
    table = new Entry[capacity];
    initHashSeedAsNeeded(capacity);
}
複製代碼

(2)roundUpToPowerOf方法說明

private static int roundUpToPowerOf2(int number) {
    //number >= 0,不能爲負數,
    //(1)number >= 最大容量:就返回最大容量
    //(2)0 =< number <= 1:返回1
    //(3)1 < number < 最大容量:
    return number >= MAXIMUM_CAPACITY
        ? MAXIMUM_CAPACITY
        : (number > 1) ? Integer.highestOneBit((number - 1) << 1) : 1;
}
//該方法和jdk8中的tabSizeFor實現基本差很少
public static int highestOneBit(int i) {
    //由於傳入的i>0,因此i的高位仍是0,這樣使用>>運算符就至關於>>>了,高位0。
    //仍是舉個例子,假設i=5=0101
    i |= (i >>  1); //(1)i>>1=0010;(2)i= 0101 | 0010 = 0111
    i |= (i >>  2); //(1)i>>2=0011;(2)i= 0111 | 0011 = 0111
    i |= (i >>  4); //(1)i>>4=0000;(2)i= 0111 | 0000 = 0111
    i |= (i >>  8); //(1)i>>8=0000;(2)i= 0111 | 0000 = 0111
    i |= (i >> 16); //(1)i>>16=0000;(2)i= 0111 | 0000 = 0111
    return i - (i >>> 1); //(1)0111>>>1=0011(2)0111-0011=0100=4
    //因此這裏返回4。
    //而在上面的roundUpToPowerOf2方法中,最後會將highestOneBit的返回值進行 << 1 操做,即最後的結果爲4<<1=8.就是返回大於number的最小2次冪
}
複製代碼

(3)putAllForCreate方法說明

​ 該方法就是遍歷傳入的map集合中的元素,而後加入本map實例中。下面咱們來看看該方法的實現細節函數

private void putAllForCreate(Map<? extends K, ? extends V> m) {
    //實際上就是遍歷傳入的map,將其中的元素添加到本map實例中(putForCreate方法實現)
    for (Map.Entry<? extends K, ? extends V> e : m.entrySet())
        putForCreate(e.getKey(), e.getValue());
}
複製代碼

​ putForCreate方法原理實現post

private void putForCreate(K key, V value) {
    //判斷key是否爲null,若是爲null那麼對應的hash爲0,不然調用剛剛上面說到的hash()方法計算hash值
    int hash = null == key ? 0 : hash(key); 
    //根據剛剛計算獲得的hash值計算在table數組中的下標
    int i = indexFor(hash, table.length);
    for (Entry<K,V> e = table[i]; e != null; e = e.next) {
        Object k;
        //hash相同,key也相同,直接用舊的值替換新的值
        if (e.hash == hash &&
            ((k = e.key) == key || (key != null && key.equals(k)))) {
            e.value = value;
            return;
        }
    }
	//這裏就是:要插入的元素的key與前面的鏈表中的key都不相同,因此須要新加一個結點加入鏈表中
    createEntry(hash, key, value, i);
}
複製代碼

(4)createEntry方法實現

void createEntry(int hash, K key, V value, int bucketIndex) {
    //這裏說的是,前面的鏈表中不存在相同的key,因此調用這個方法建立一個新的結點,而且結點所在的桶
    //bucket的下標指定好了
    Entry<K,V> e = table[bucketIndex];
    /*Entry(int h, K k, V v, Entry<K,V> n) {value = v;next = n;key = k;hash = h;}*/
    table[bucketIndex] = new Entry<>(hash, key, value, e);//Entry的構造器,建立一個新的結點做爲頭節點(頭插法)
    size++;//將當前hash表中的數量加1
}
複製代碼

怎樣肯定元素在桶中的位置

​ 1.7中的計算hash值的算法和1.8的實現是不同的,而hash值又關係到咱們put新元素的位置、get查找元素、remove刪除元素的時候去經過indexFor查找下標。因此咱們來看看這兩個方法this

(1)hash方法

final int hash(Object k) {
    int h = hashSeed;
    //默認是0,不是0那麼須要key是String類型才使用stringHash32這種hash方法
    if (0 != h && k instanceof String) {
        return sun.misc.Hashing.stringHash32((String) k);
    }
    //這段代碼是爲了對key的hashCode進行擾動計算,防止不一樣hashCode的高位不一樣但低位相同致使的hash衝突。簡單點
    //說,就是爲了把高位的特徵和低位的特徵組合起來,下降哈希衝突的機率,也就是說,儘可能作到任何一位的變化都能對
    //最終獲得的結果產生影響
    h ^= k.hashCode();
    h ^= (h >>> 20) ^ (h >>> 12);
    return h ^ (h >>> 7) ^ (h >>> 4);
}
複製代碼

​ 咱們經過下面的例子來講明對於key的hashCode進行擾動處理的重要性,咱們如今想向一個map中put一個Key-Value對,Key的值爲「fsmly」,不進行任何的擾動處理知識單純的通過簡單的獲取hashcode後,獲得的值爲「0000_0000_0011_0110_0100_0100_1001_0010」,若是當前map的中的table數組長度爲16,最終獲得的index結果值爲10。因爲15的二進制擴展到32位爲「00000000000000000000000000001111」,因此,一個數字在和他進行按位與操做的時候,前28位不管是什麼,計算結果都同樣(由於0和任何數作與,結果都爲0,那這樣的話一個put的Entry結點就太過依賴於key的hashCode的低位值,產生衝突的機率也會大大增長)。以下圖所示

​ 由於map的數組長度是有限的,這樣衝突機率大的方法是不適合使用的,因此須要對hashCode進行擾動處理下降衝突機率,而JDK7中對於這個處理使用了四次位運算,仍是經過下面的簡單例子看一下這個過程.能夠看到,剛剛不進行擾動處理的hashCode在進行處理後就沒有產生hash衝突了。

​ 總結一下:咱們會首先計算傳入的key的hash值而後經過下面的indexFor方法肯定在table中的位置,具體實現就是經過一個計算出來的hash值和length-1作位運算,那麼對於2^n來講,長度減一轉換成二進制以後就是低位全一(長度16,len-1=15,二進制就是1111)。上面四次擾動的這種設定的好處就是,對於獲得的hashCode的每一位都會影響到咱們索引位置的肯定,其目的就是爲了能讓數據更好的散列到不一樣的桶中,下降hash衝突的發生。關於Java集合中存在hash方法的更多原理和細節,請參考這篇hash()方法分析

(2)indexFor方法

static int indexFor(int h, int length) {
    //仍是使用hash & (n - 1)計算獲得下標
    return h & (length-1);
}
複製代碼

主要實現就是將計算的key的hash值與map中數組長度length-1進行按位與運算,獲得put的Entry在table中的數組下標。具體的計算過程在上面hash方法介紹的時候也有示例,這裏就不贅述了。

put方法分析

(1)put方法

public V put(K key, V value) {
    //咱們知道Hash Map有四中構造器,而只有一種(參數爲map的)初始化了table數組,其他三個構造器只
    //是賦值了閾值和加載因子,因此使用這三種構造器建立的map對象,在調用put方法的時候table爲{},
    //其中沒有元素,因此須要對table進行初始化
    if (table == EMPTY_TABLE) {
        //調用inflateTable方法,對table進行初始化,table的長度爲:
        //不小於threshold的最小的2的冪數,最大爲MAXIMUM_CAPACITY
        inflateTable(threshold);
    }
    //若是key爲null,表示插入一個鍵爲null的K-V對,須要調用putForNullKey方法
    if (key == null)
        return putForNullKey(value);
    
    //計算put傳入的key的hash值
    int hash = hash(key);
    //根據hash值和table的長度計算所在的下標
    int i = indexFor(hash, table.length);
    //從數組中下標爲indexFor(hash, table.length)處開始(1.7中是用鏈表解決hash衝突的,這裏就
    //是遍歷鏈表),實際上就是已經定位到了下標i,這時候就須要處理可能出現hash衝突的問題
    for (Entry<K,V> e = table[i]; e != null; e = e.next) {
        Object k;
        //hash值相同,key相同,替換該位置的oldValue爲value
        if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
            V oldValue = e.value;
            e.value = value;
            //空方法,讓其子類重寫
            e.recordAccess(this);
            return oldValue;
        }
    }
	//若是key不相同,即在鏈表中沒有找到相同的key,那麼須要將這個結點加入table[i]這個鏈表中
    
    //修改modCount值(後續總結文章會說到這個問題)
    modCount++;
    //遍歷沒有找到該key,就調用該方法添加新的結點
    addEntry(hash, key, value, i);
    return null;
}
複製代碼

(2)putForNullKey方法分析

​ 這個方法是處理key爲null的狀況的,當傳入的key爲null的時候,會在table[0]位置開始遍歷,遍歷的其實是當前以table[0]爲head結點的鏈表,若是找到鏈表中結點的key爲null,那麼就直接替換掉舊值爲傳入的value。不然建立一個新的結點而且加入的位置爲table[0]位置處。

//找到table數組中key爲null的那個Entry對象,而後將其value進行替換
private V putForNullKey(V value) {
    //從table[0]開始遍歷
    for (Entry<K,V> e = table[0]; e != null; e = e.next) {
        //key爲null
        if (e.key == null) {
            //將value替換爲傳遞進來的value
            V oldValue = e.value;
            e.value = value;
            e.recordAccess(this);
            return oldValue; //返回舊值
        }
    }
    modCount++;
    //若不存在,0位置桶上的鏈表中添加新結點
    addEntry(0, null, value, 0);
    return null;
}
複製代碼

(3)addEntry方法分析

​ addEntry方法的主要做用就是判斷當前的size是否大於閾值,而後根據結果判斷是否擴容,最終建立一個新的結點插入在鏈表的頭部(實際上就是table數組中的那個指定下標位置處)

/* hashmap採用頭插法插入結點,爲何要頭插而不是尾插,由於後插入的數據被使用的頻次更高,而單鏈表沒法隨機訪問只能從頭開始遍歷查詢,因此採用頭插.忽然又想爲何不採用二維數組的形式利用線性探查法來處理衝突,數組末尾插入也是O(1),可數組其最大缺陷就是在於若不是末尾插入刪除效率很低,其次若添加的數據分佈均勻那麼每一個桶上的數組都須要預留內存. */
void addEntry(int hash, K key, V value, int bucketIndex) {
    //這裏有兩個條件
    //①size是否大於閾值
    //②當前傳入的下標在table中的位置不爲null
    if ((size >= threshold) && (null != table[bucketIndex])) {
        //若是超過閾值須要進行擴容
        resize(2 * table.length);
        //下面是擴容以後的操做
        //計算不爲null的key的hash值,爲null就是0
        hash = (null != key) ? hash(key) : 0;
        //根據hash計算下標
        bucketIndex = indexFor(hash, table.length);
    }
    //執行到這裏表示(可能已經擴容也可能沒有擴容),建立一個新的Entry結點
    createEntry(hash, key, value, bucketIndex);
}
複製代碼

(4)總結put方法的執行流程

  1. 首先判斷數組是否爲空,若爲空調用inflateTable進行擴容.
  2. 接着判斷key是否爲null,若爲null就調用putForNullKey方法進行put.(這裏也說明HashMap容許key爲null,默認插入在table中位置爲0處)
  3. 調用hash()方法,將key進行一次哈希計算,獲得的hash值和當前數組長度進行&計算獲得數組中的索引
  4. 而後遍歷該數組索引下的鏈表,若key的hash和傳入key的hash相同且key的equals放回true,那麼直接覆蓋 value
  5. 最後若不存在,那麼在此鏈表中頭插建立新結點

resize方法分析

(1)resize的大致流程

void resize(int newCapacity) {
    //獲取map中的舊table數組暫存起來
    Entry[] oldTable = table;
    //獲取原table數組的長度暫存起來
    int oldCapacity = oldTable.length;
    //若是原table的容量已經超過了最大值,舊直接將閾值設置爲最大值
    if (oldCapacity == MAXIMUM_CAPACITY) {
        threshold = Integer.MAX_VALUE;
        return;
    }
	//以傳入的新的容量長度爲新的哈希表的長度,建立新的數組
    Entry[] newTable = new Entry[newCapacity];
    //調用transfer
    transfer(newTable, initHashSeedAsNeeded(newCapacity));
    //table指向新的數組
    table = newTable;
    //更新閾值
    threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}
複製代碼

(2)transfer方法分析

​ transfer方法遍歷舊數組全部Entry,根據新的容量逐個從新計算索引頭插保存在新數組中。

void transfer(Entry[] newTable, boolean rehash) {
    //新數組的長度
    int newCapacity = newTable.length;
    //遍歷舊數組
    for (Entry<K,V> e : table) {
        while(null != e) {
            Entry<K,V> next = e.next;
            if (rehash) {
                //從新計算hash值
                e.hash = null == e.key ? 0 : hash(e.key);
            }
            //這裏根據剛剛獲得的新hash從新調用indexFor方法計算下標索引
            int i = indexFor(e.hash, newCapacity);
            //假設當前數組中某個位置的鏈表結構爲a->b->c;women 
            //(1)當爲原鏈表中的第一個結點的時候:e.next=null;newTable[i]=e;e=e.next
            //(2)當遍歷到原鏈表中的後續節點的時候:e.next=head;newTable[i]=e(這裏將頭節點設置爲新插入的結點,即頭插法);e=e.next
            //(3)這裏也是致使擴容後,鏈表順序反轉的原理(代碼就是這樣寫的,鏈表反轉,固然前提是計算的新下標仍是相同的)
            e.next = newTable[i]; 
            newTable[i] = e;
            e = next;
        }
    }
}
複製代碼

​ 這個方法的主要部分就是,在從新計算hash以後對於原鏈表和新table中的鏈表結構的差別,咱們經過下面這個簡單的圖理解一下,假設原table中位置爲4處爲一個鏈表entry1->entry2->entry3,三個結點在新數組中的下標計算仍是4,那麼這個流程大概以下圖所示

(3)resize擴容方法總結

  1. ​ 建立一個新的數組(長度爲原長度爲2倍,若是已經超過最大值就設置爲最大值)
  2. 調用transfer方法將entry從舊的table中移動到新的數組中,具體細節如上所示
  3. 將table指向新的table,更新閾值

get方法分析

//get方法,其中調用的是getEntry方法沒若是不爲null就返回對應entry的value
public V get(Object key) {
    if (key == null)
        return getForNullKey();
    Entry<K,V> entry = getEntry(key);
    return null == entry ? null : entry.getValue();
}
複製代碼

​ 能夠看到,get方法中是調用getEntry查詢到Entry對象,而後返回Entry的value的。因此下面看看getEntry方法的實現

getEntry方法

//這是getEntry的實現
final Entry<K,V> getEntry(Object key) {
    //沒有元素天然返回null
    if (size == 0) {
        return null;
    }
	//經過傳入的key值調用hash方法計算哈希值
    int hash = (key == null) ? 0 : hash(key);
    //計算好索引以後,從對應的鏈表中遍歷查找Entry
    for (Entry<K,V> e = table[indexFor(hash, table.length)];
         e != null;
         e = e.next) {
        Object k;
        //hash相同,key相同就返回
        if (e.hash == hash &&
            ((k = e.key) == key || (key != null && key.equals(k))))
            return e;
    }
    return null;
}
複製代碼

getForNullKey方法

//這個方法是直接查找key爲null的
private V getForNullKey() {
    if (size == 0) {
        return null;
    }
    //直接從table中下標爲0的位置處的鏈表(只有一個key爲null的)開始查找
    for (Entry<K,V> e = table[0]; e != null; e = e.next) {
        //key爲null,直接返回對應的value
        if (e.key == null)
            return e.value;
    }
    return null;
}
複製代碼

jdk1.7版本的實現簡單總結

​ (1)由於其put操做對key爲null場景使用putForNullKey方法作了單獨處理,HashMap容許null做爲Key

​ (2)在計算table的下標的時候,是根據key的hashcode值調用hash()方法以後獲取hash值與數組length-1進行&運算,length-1的二進制位全爲1,這是爲了可以均勻分佈,避免衝突(長度要求爲2的整數冪次方) ​ (3)不論是get仍是put以及resize,執行過程當中都會對key的hashcode進行hash計算,而可變對象其hashcode很容易變化,因此HashMap建議用不可變對象(如String類型)做爲Key. ​ (4)HashMap是線程不安全的,在多線程環境下擴容時候可能會致使環形鏈表死循環,因此若須要多線程場景下操做可使用ConcurrentHashMap(下面咱們經過圖示簡單演示一下這個狀況) ​ (5)當發生衝突時,HashMap採用鏈地址法處理衝突 ​ (6)HashMap初始容量定爲16,簡單認爲是8的話擴容閾值爲6,閾值過小致使擴容頻繁;而32的話可能空間利用率低。

jdk7中併發狀況下的環形鏈表問題圖解

​ 上面在說到resize方法的時候,咱們也經過圖示實例講解了一個resize的過程,因此這裏咱們就再也不演示單線程下面的執行流程了。咱們首先記住resize方法中的幾行核心代碼

Entry<K,V> next = e.next;
//省略從新計算hash和index的兩個過程...
e.next = newTable[i]; 
newTable[i] = e;
e = next;
複製代碼

​ resize方法中調用的transfer方法的主要幾行代碼就是上面的這四行,下來簡單模擬一下假設兩個線程thread1和thread2執行了resize的過程.

​ (1)resize以前,假設table長度爲2,假設如今再添加一個entry4,就須要擴容了

​ (2)假設如今thread1執行到了 **Entry<K,V> next = e.next;**這行代碼處,那麼根據上面幾行代碼,咱們簡單作個註釋

​ (3)而後因爲線程調度輪到thread2執行,假設thread2執行完transfer方法(假設entry3和entry4在擴容後到了以下圖所示的位置,這裏咱們主要關注entry1和entry2兩個結點),那麼獲得的結果爲

(4)此時thread1被調度繼續執行,將entry1插入到新數組中去,而後e爲Entry2,輪到下次循環時next因爲Thread2的操做變爲了Entry1

  • **先是執行 newTalbe[i] = e;**在thread1執行時候,e指向的是entry1
  • 而後是e = next,致使了e指向了entry2(next指向的是entry2)
  • 而下一次循環的next = e.next,(即next=entry2.next=entry1這是thread2執行的結果)致使了next指向了entry1

以下圖所示

​ (5)thread1繼續執行,將entry2拿下來,放在newTable[1]這個桶的第一個位置,而後移動e和next

(6)e.next = newTable[1] 致使 entry1.next 指向了 entry2,也要注意,此時的entry2.next 已經指向了entry1(thread2執行的結果就是entry2->entry1,看上面的thread2執行完的示意圖), 環形鏈表就這樣出現了。

相關文章
相關標籤/搜索