HashMap源碼分析(jdk7)


HashMap的結構圖示

​ jdk1.7的HashMap采用數組+單鏈表實現,盡管定義了hash函數來避免沖突,但因為數組長度有限,還是會出現兩個不同的Key經過計算后在數組中的位置一樣,1.7版本中采用了鏈表來解決。

​ 從上面的簡易示圖中也能發現,如果位於鏈表中的結點過多,那么很顯然通過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進行初始化

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方法原理實現

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
}

HashMap確定元素在數組中的位置

​ 1.7中的計算hash值的算法和1.8的實現是不一樣的,而hash值又關系到我們put新元素的位置、get查找元素、remove刪除元素的時候去通過indexFor查找下標。所以我們來看看這兩個方法

(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方法介紹的時候也有示例,這里就不贅述了。

HashMap的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. 最后若不存在,那么在此鏈表中頭插創建新結點

HashMap的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,更新閾值

HashMap的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;
}

jdk7版本的實現簡單總結

​ (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執行完的示意圖), 環形鏈表就這樣出現了。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM