1. 程式人生 > >HashMap原始碼分析(jdk7)

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執行完的示意圖), 環形連結串列就這樣出現了。

相關推薦

HashMap原始碼分析jdk7

HashMap的結構圖示 ​ jdk1.7的HashMap採用陣列+單鏈表實現,儘管定義了hash函式來避免衝突,但因為陣列長度有限,還是會出現兩個不同的Key經過計算後在陣列中的位置一樣,1.7版本中採用了連結串列來解決。 ​ 從上面的簡易示圖中也能發現,如果位於連結串列中的結點過多,那麼很顯然通過ke

HashMap原始碼分析put-jdk8-紅黑樹的引入

HashMap jdk8以後他的邏輯結構發生了一點變化: 大概就是這個意思: 當某一個點上的元素數量打到一定的閾值的時候,連結串列會變成一顆樹,這樣在極端情況下(所有的元素都在一個點上,整個就以連結串列),一些操作的時間複雜度有O(n)變成了O(logn)。 分析原始碼

HashMap原始碼分析

HashMap是一種散列表,通過陣列加連結串列的方式實現。在JDK1.8版本後新增加了紅黑樹結構來提高效率。 HashMap HashMap是Java的Map介面的一種基礎雜湊表實現。它提供了Map的所有操作,並且支援以null為key或value。(除了是非同步的以及支援null以外,HashMap與H

HashMap實現原理及原始碼分析轉載

作者: dreamcatcher-cx 出處: <http://www.cnblogs.com/chengxiao/>        雜湊表(hash table)也叫散列表,是一種非常重要的資料結構,應用場景及其豐富,

java集合之----HashMap原始碼分析基於JDK1.7與1.8

一、什麼是HashMap 百度百科這樣解釋: 簡而言之,HashMap儲存的是鍵值對(key和value),通過key對映到value,具有很快的訪問速度。HashMap是非執行緒安全的,也就是說在多執行緒併發環境下會出現問題(死迴圈) 二、內部實現 (1)結構 HashM

集合原始碼分析HashMap集合

1、HashMap概述: 底層是雜湊演算法,針對鍵。HashMap允許null鍵和null值,執行緒不安全,效率高。鍵不可以重複儲存,值可以。 雜湊結構:不能保證資料的迭代順序,也不能保證順序的恆久不變。 Map集合(無序、無索引、不可以重複)是雙列集合,一個鍵對應一個

通俗易懂的JDK1.8中HashMap原始碼分析歡迎探討指正+ 典型面試題

面試題在最下面 說到HashMap之前,閱讀ArrayList與LinkedList的原始碼後做個總結 ArrayList 底層是陣列,查詢效率高,增刪效率低 LinkedList底層是雙鏈表,查詢效率低,增刪效率高   這裡只是總結看完原始碼後對hashm

JDK原始碼分析4HashMap

JDK版本 HashMap簡介 HashMap基於雜湊表的 Map 介面的實現。此實現提供所有可選的對映操作,並允許使用 null 值和 null 鍵。(除了不同步和允許使用 null 之外,HashMap 類與 Hashtable 大致相同。)此類不保證對映

JDK原始碼分析3HashMap

JDK版本 HashMap簡介 HashMap基於雜湊表的 Map 介面的實現。此實現提供所有可選的對映操作,並允許使用 null 值和 null 鍵。(除了不同步和允許使用 null 之外,HashMap 類與 Hashtable 大致相同。)此類不保證對映的順序,特別是它不保證該順序恆久不變。

HashMap原始碼分析JDK1.8- 你該知道的都在這裡了

       HashMap是Java和Android程式設計師的基本功, JDK1.8對HashMap進行了優化, 你真正理解它了嗎? 考慮如下問題:  1、雜湊基本原理?(答:散列表、hash碰撞、連結串列、紅黑樹)2、hashmap查詢的時間複雜度, 影響因素和原理?

jdk1.8 HashMap原始碼分析resize函式

final Node<K,V>[] resize() { Node<K,V>[] oldTab = table; int oldCap = (oldTab == null) ? 0 : oldTab.len

java集合4HashMap原始碼分析jdk1.8

前言 Map介面雖然也是集合體系中的重要一個分支,但是Map介面並不繼承自Collection,而是自成一派。 public interface Map<K,V> Map集合儲存鍵對映到值的物件。一個集合中不能包含重複的鍵,每個鍵最多

HashMap原始碼分析jdk1.8

private static final long serialVersionUID = 362498820763181265L;      //The default initial capacity - MUST be a power of two. static final 

HashMap原始碼分析基於JDK8

HashMap簡介          HashMap基於雜湊表的 Map 介面的實現。此實現提供所有可選的對映操作,並允許使用 null 值和 null 鍵。(除了不同步和允許使用 null 之外,HashMap 類與 Hashtable 大致相同。)此類不保證對映的順序,

基於jdk1.8的HashMap原始碼分析溫故學習

鼎力推薦一下一. HashMap結構      HashMap在jdk1.6版本採用陣列+連結串列的儲存方式,但是到1.8版本時採用了陣列+連結串列/紅黑樹的方式進行儲存,有效的提高了查詢時間,解決衝突。這裡有一篇部落格寫的非常好,HashMap的結構圖也畫的非常清楚,鼎力推

HashMap原始碼分析基於1.8

HashMap1.7和1.8變動比較多。 關於HashMap 1.7的版本,倪升武的部落格總結的很好。 這裡我主要來介紹一下1.8中的HashMap。由於HashMap原始碼太長,我只挑選了部分進行分析,如果有沒有分析到的重點難點或者大家有疑問的地方,希望大

Java 集合原始碼分析HashMap

目錄 Java 集合原始碼分析(一)HashMap 1. 概要 2. JDK 7 的 HashMap 3. JDK 1.8 的 HashMap 4. Hashtable 5.

HashMap原始碼分析史上最詳細的原始碼分析

HashMap簡介 HashMap是開發中使用頻率最高的用於對映(鍵值對 key value)處理的資料結構,我們經常把hashMap資料結構叫做雜湊連結串列;  ObjectI entry<Key,Value>,entry<Key,Value>] 可以將資料通過鍵值對形

Java入門系列之集合HashMap原始碼分析十四

前言 我們知道在Java 8中對於HashMap引入了紅黑樹從而提高操作效能,由於在上一節我們已經通過圖解方式分析了紅黑樹原理,所以在接下來我們將更多精力投入到解析原理而不是演算法本身,HashMap在Java中是使用比較頻繁的鍵值對資料型別,所以我們非常有必要詳細去分析背後的具體實現原理,無論是C#還是J

Java HashMap原始碼分析含散列表、紅黑樹、擾動函式等重點問題分析

# 寫在最前面 這個專案是從20年末就立好的 flag,經過幾年的學習,回過頭再去看很多知識點又有新的理解。所以趁著找實習的準備,結合以前的學習儲備,建立一個主要針對應屆生和初學者的 Java 開源知識專案,專注 Java 後端面試題 + 解析 + 重點知識詳解 + 精選文章的開源專案,希望它能伴隨你我一直