自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

徹底搞懂hashMap底層原理

開發(fā)
HashMap的數(shù)據(jù)結(jié)構(gòu)是由數(shù)組和鏈表組成,table是一個(gè)存放Entry對(duì)象的數(shù)組,每個(gè)Entry對(duì)象由4個(gè)屬性組成。

一、說明

hashMap在java1.7和java1.8版本中有做一些調(diào)整,我們本篇只說java1.7的hashMap。

二、數(shù)據(jù)結(jié)構(gòu)

hashMap的數(shù)據(jù)結(jié)構(gòu)是由數(shù)組和鏈表組成,table是一個(gè)存放Entry對(duì)象的數(shù)組,每個(gè)Entry對(duì)象由4個(gè)屬性組成,分別是key、value、next、hash,key和value是我們熟知的鍵值對(duì),不需要過多解釋,next是當(dāng)前元素在鏈表中指向下一個(gè)元素的引用,hash是計(jì)算出來的hashcode,hashMap中的hsah是通過對(duì)key.hashcode()進(jìn)行一定操作得出的,并不是直接使用key.hashcode()方法計(jì)算數(shù)來的值。

三、屬性信息

先來了解下hashMap中一些重要的屬性:

//Hashmap的初始化大小,初始化的值為16,1往右移4位為16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; 

//HashMap是動(dòng)態(tài)擴(kuò)容的,就是容量大小不能大于 1<<30
static final int MAXIMUM_CAPACITY = 1 << 30;

//默認(rèn)的擴(kuò)容因子,這個(gè)值可以通過構(gòu)造修改
static final float DEFAULT_LOAD_FACTOR = 0.75f;

//空數(shù)據(jù),默認(rèn)構(gòu)造的時(shí)候賦值為空的Entry數(shù)組,在添加元素的時(shí)候
//會(huì)判斷table=EMPTY_TABLE ,然后就擴(kuò)容
static final Entry<?,?>[] EMPTY_TABLE = {};

//表示一個(gè)空的Hashmap
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;

//Hashmap的大小
transient int size;

//threshold表示當(dāng)HashMap的size大于threshold時(shí)會(huì)執(zhí)行resize操作。
DEFAULT_INITIAL_CAPACITY=16

//擴(kuò)容的閾值
int threshold;

//擴(kuò)容因子,沒有傳入就使用默認(rèn)的DEFAULT_LOAD_FACTOR = 0.75f
final float loadFactor;

//數(shù)據(jù)操作次數(shù),用于迭代檢查修改異常
transient int modCount;

static final int ALTERNATIVE_HASHING_THRESHOLD_DEFAULT = Integer.MAX_VALUE;

四、put方法

步驟:

  1. 判斷table是否為空,如果為空則初始化,不為空則下一步
  2. 判斷key是否為null,如果為null則處理,不為null則下一步
  3. 根據(jù)key計(jì)算下標(biāo)
  4. 如果下標(biāo)處的桶不為空,則從下標(biāo)處開始遍歷鏈表,如果找到key相同的則更新覆蓋并返回舊值,如果為空則下一步
  5. 插入前判斷是否需要擴(kuò)容,如果需要?jiǎng)t進(jìn)行擴(kuò)容,如果不需要就下一步
  6. 頭插法插入桶中

接下來我們對(duì)每一步驟分別展開討論。

1.table初始化

private void inflateTable(int toSize) {
    // 找到大于等于指定數(shù)組長度的2的n次方
    int capacity = roundUpToPowerOf2(toSize);
 // 擴(kuò)容閾值,數(shù)組長度*負(fù)載因子
    threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
    // 創(chuàng)建數(shù)組
    table = new Entry[capacity];
    // 是否重新賦值hashSeed 
    initHashSeedAsNeeded(capacity);
}

private static int roundUpToPowerOf2(int number) {
    // 使用Integer的highestOneBit方法找到大于等于指定數(shù)組長度的2的n次方
    return number >= MAXIMUM_CAPACITY
            ? MAXIMUM_CAPACITY
            : (number > 1) ? Integer.highestOneBit((number - 1) << 1) : 1;
}

數(shù)組初始化其實(shí)就三個(gè)步驟:計(jì)算數(shù)組容量,創(chuàng)建數(shù)組,判斷是否重hash。

(1)「數(shù)組容量」: 如果指定數(shù)組長度值大于MAXIMUM_CAPACITY(最大數(shù)組容量:2的30次方),就用最大值;如果指定數(shù)組長度值小于等于1,就用1;如果指定數(shù)組長度值大于1,就用下面的方法得到大于等于指定數(shù)組長度的第一個(gè)2的n次方的值。

Integer.highestOneBit((number - 1) << 1)

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);
    }

這個(gè)方法內(nèi)部是通過位移和亦或操作得到的值,感興趣的可以直接看下源碼。

(2) 「創(chuàng)建數(shù)組」:直接用計(jì)算出來的數(shù)組長度創(chuàng)建Entry數(shù)組table,元素類型為Entry。

(3) 「判斷是否重hash」:

重hash就是對(duì)同個(gè)key重新計(jì)算hash值,那么為什么要重新計(jì)算hash值,實(shí)際上只是為了讓hsah值更復(fù)雜,在計(jì)算下標(biāo)的時(shí)候就會(huì)更加散列,減少hash沖突。

那么什么樣的條件下才會(huì)重hash呢,從源碼可以看出switching為true表示需要重hash,影響switching取值的是下面這段代碼:

final boolean initHashSeedAsNeeded(int capacity) {
 // hashSeed 初始值為0,false
    boolean currentAltHashing = hashSeed != 0;
    // 數(shù)組長度是否 >= 2^31-1 
    boolean useAltHashing = sun.misc.VM.isBooted() &&
            (capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
    // 使用異或,currentAltHashing 為false,只有數(shù)組長度>= 2^31-1時(shí)返回true
    boolean switching = currentAltHashing ^ useAltHashing;
    //switching為true,則hashSeed重新賦值(一般不會(huì)出現(xiàn))
    if (switching) {
        hashSeed = useAltHashing
            ? sun.misc.Hashing.randomHashSeed(this)
            : 0;
    }
    return switching;
}

initHashSeedAsNeeded方法用來判斷是否進(jìn)行重hash,如果需要重hash,會(huì)同步更新hash種子,最后返回boolean類型的值。

  • sun.misc.VM.isBooted()指jvm運(yùn)行狀態(tài),一般為true;
  • hashSeed初始為0,所以currentAltHashing一定為false;

Holder.ALTERNATIVE_HASHING_THRESHOLD取的是環(huán)境變量jdk.map.althashing.threshold配置的值(程序員配置),如果沒有配置就默認(rèn)取Integer.MAX_VALUE。

通過上面的分析可以知道是否進(jìn)行重hash只會(huì)受到capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD的影響。

「我們可以得出一個(gè)結(jié)論:」如果程序員不配置環(huán)境變量jdk.map.althashing.threshold,那么就永遠(yuǎn)不會(huì)進(jìn)行重hash,因?yàn)閿?shù)組長度capacity最大是2的30次方,而Integer.MAX_VALUE的值是2的31次方減1,即這個(gè)條件永遠(yuǎn)不會(huì)滿足,但是你可能會(huì)說擴(kuò)容的時(shí)候傳入的capacity正好是最大值2的30次方的2倍,但是我會(huì)告訴你,如果數(shù)組成都達(dá)到2的30次方,是不允許擴(kuò)容的。

所以說如果程序員不設(shè)置環(huán)境變量,initHashSeedAsNeeded方法實(shí)際上沒有意義的。

那為什么要更新hash種子呢?

這就要了解下hsah值是怎么生成的了:

final int hash(Object k) {
    int h = hashSeed;
    if (0 != h && k instanceof String) {
        return sun.misc.Hashing.stringHash32((String) k);
    }
 
    h ^= k.hashCode();
    h ^= (h >>> 20) ^ (h >>> 12);
    return h ^ (h >>> 7) ^ (h >>> 4);
}

hashMap的hsah值是由key調(diào)用自身的hashCode方法得到的值與hashSeed進(jìn)行5次亦或操作4次位移運(yùn)算得到的。

所以相同的key只有在hashSeed變化后才有生成不同的hash值,否則生成的永遠(yuǎn)是相同的hsah值,所以需要重hash的時(shí)候就必須改變hashSeed,否則重hash的結(jié)果會(huì)和原來一樣。

2.處理key為null

private V putForNullKey(V value) {
 // 當(dāng)key為null時(shí),數(shù)組下標(biāo)指定為0
    for (Entry<K,V> e = table[0]; e != null; e = e.next) {
     // 判斷現(xiàn)在有沒有key為null的Entry
        if (e.key == null) {
         //value替換為新值,返回舊的value
            V oldValue = e.value;
            e.value = value;
            e.recordAccess(this);
            return oldValue;
        }
    }
    //修改次數(shù)+1
    modCount++;
    // table[0]中沒有值或沒有匹配到null的key,創(chuàng)建一個(gè)Entry放入table[0]
    addEntry(0, null, value, 0);
    return null;
}

上面這段代碼是對(duì)key為null的情況的處理,同時(shí)也說明hashmap是允許key為null的,通過上面可以看出hashMap中key為null的元素只會(huì)存儲(chǔ)在數(shù)組下標(biāo)為0的桶中,如果桶中有多個(gè),就遍歷鏈表找到key為null的元素進(jìn)行覆蓋更新,如果桶中無元素,就調(diào)用addEntry方法插入元素,這里只需要知道調(diào)用addEntry方法的結(jié)果是將數(shù)據(jù)插入數(shù)組下標(biāo)為0的桶中,addEntry方法我們下面再具體看。

3.計(jì)算下標(biāo)

// 獲取key的hash值
    int hash = hash(key);
    // 根據(jù)hash值和數(shù)組長度計(jì)算要放入的數(shù)組下標(biāo)位置
    int i = indexFor(hash, table.length);

計(jì)算下標(biāo)實(shí)際上分為兩步,計(jì)算hsah值和計(jì)算下標(biāo),計(jì)算下標(biāo)的原理是用hash值除以數(shù)組容量,得到的余數(shù)就是下標(biāo),用這種方式可以保證不同的key一定會(huì)放在數(shù)組中的某個(gè)桶中,不會(huì)越界,而hash值可以讓不同的key在數(shù)組中的分部足夠分散,減少hsah沖突。

final int hash(Object k) {
    int h = hashSeed;
    if (0 != h && k instanceof String) {
        return sun.misc.Hashing.stringHash32((String) k);
    }
 
    h ^= k.hashCode();
    h ^= (h >>> 20) ^ (h >>> 12);
    return h ^ (h >>> 7) ^ (h >>> 4);
}

上面已將說過hash方法,這里再來說一次,我們知道java中每個(gè)類都默認(rèn)由hashcode方法生成hashcode,但是hashMap中并沒有直接用此方法生成的hashcode,而是對(duì)其生成的hahshcode與hash種子進(jìn)行亦或和位移操作,1.7的hashMap在計(jì)算hsah的時(shí)候進(jìn)行了5次亦或操作和4次位移運(yùn)算,這樣做的目的就是使得不同的key計(jì)算出來的hash更加分散,更加能減少hash沖突。

static int indexFor(int h, int length) {
    // 計(jì)算數(shù)組下標(biāo)位置,數(shù)組長度必須為2的n次方
    return h & (length-1);
}

我們上面說了hash除以數(shù)組容量得到數(shù)組下標(biāo),但是這種做法在java中太慢了,而和此做法同效的一種方式就是h & (length-1),就是數(shù)組容量減去1,再與hash做&操作。這種方式在java中是非常高效的。

4.遍歷查找key

運(yùn)行到這里,數(shù)組已經(jīng)有了,key對(duì)應(yīng)的下標(biāo)也有了,接下來就是插入操作,插入之前會(huì)先查看下標(biāo)對(duì)應(yīng)的桶中是否為空桶,如果不為空桶就要先遍歷查找是否有相同key。

for (Entry<K,V> e = table[i]; e != null; e = e.next) {
        Object k;
        // 判斷key的值是否相等
        if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
         // key的值相等則寫入新的value值,將舊value返回
            V oldValue = e.value;
            e.value = value;
            e.recordAccess(this);
            return oldValue;
        }
    }
    
   // 修改次數(shù)+1
    modCount++;
    // 下標(biāo)位置為空或沒有能夠匹配的key值,創(chuàng)建Entry放入鏈表
    addEntry(hash, key, value, i);

因?yàn)閔ashMap是由數(shù)組和鏈表組成,數(shù)組的每個(gè)桶中都由鏈表組成,所以需要遍歷鏈表查找相同的key,如果存在相同的key就更新覆蓋,如果沒有,就調(diào)用addEntry方法進(jìn)行插入。

//addEntry方法
void addEntry(int hash, K key, V value, int bucketIndex) {
 // 如果當(dāng)前數(shù)組長度>=擴(kuò)容閾值 并且 當(dāng)前數(shù)組下標(biāo)位置不為null
    if ((size >= threshold) && (null != table[bucketIndex])) {
     // 數(shù)組擴(kuò)容為當(dāng)前長度*2
        resize(2 * table.length);
        // key是否為null,不為null計(jì)算hash值,null則直接hash值為0
        hash = (null != key) ? hash(key) : 0;
        // 根據(jù)hash值和新數(shù)組長度計(jì)算下標(biāo)位置
        bucketIndex = indexFor(hash, table.length);
    }
 //創(chuàng)建Entry放入table中
    createEntry(hash, key, value, bucketIndex);
}

可以看到在正式添加元素之前會(huì)先判斷是否需要擴(kuò)容,如果需要?jiǎng)t先進(jìn)行擴(kuò)容。

5.擴(kuò)容處理

從上面的源碼可以知道擴(kuò)容需要滿足兩個(gè)條件:

  • 數(shù)組長度達(dá)到threshold,threshold是由負(fù)載因子和數(shù)組容量計(jì)算出來的
  • 當(dāng)前數(shù)組下標(biāo)對(duì)應(yīng)的桶不為null

如果滿足條件則進(jìn)入resize方法進(jìn)行擴(kuò)容:

void resize(int newCapacity) {
 // 原數(shù)組
    Entry[] oldTable = table;
    // 原數(shù)組長度
    int oldCapacity = oldTable.length;
    // 如果原數(shù)組長度為2^30,不進(jìn)行擴(kuò)容,把擴(kuò)容閾值修改為2^31-1
    if (oldCapacity == MAXIMUM_CAPACITY) {
        threshold = Integer.MAX_VALUE;
        return;
    }
 // 根據(jù)新的數(shù)組長度,創(chuàng)建數(shù)組
    Entry[] newTable = new Entry[newCapacity];
    // 轉(zhuǎn)移數(shù)組數(shù)據(jù) 
    // 調(diào)用initHashSeedAsNeeded方法,根據(jù)新數(shù)組的長度判斷是否會(huì)hashSeed重新賦值
    transfer(newTable, initHashSeedAsNeeded(newCapacity));
    // table指向新數(shù)組
    table = newTable;
    // 計(jì)算新的擴(kuò)容閾值
    threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}

// transfer方法
void transfer(Entry[] newTable, boolean rehash) {
 // 新數(shù)組長度
    int newCapacity = newTable.length;
    // 遍歷原數(shù)組的Entry
    for (Entry<K,V> e : table) {
     // Entry不為null
        while(null != e) {
         // 先找到鏈表中下一個(gè)要轉(zhuǎn)移的Entry
            Entry<K,V> next = e.next;
            // 如果hashSeed變了,重新進(jìn)行hash計(jì)算
            if (rehash) {
                e.hash = null == e.key ? 0 : hash(e.key);
            }
            // 重新計(jì)算數(shù)組下標(biāo),結(jié)果分兩種:1.與原下標(biāo)一直 2.原下標(biāo)+本次擴(kuò)容了多長
            int i = indexFor(e.hash, newCapacity);
            /*原數(shù)組統(tǒng)一鏈表中的數(shù)據(jù)轉(zhuǎn)移到新數(shù)組中時(shí),所處鏈表順序顛倒,因此多線程的情況下可能會(huì)出現(xiàn)環(huán)形鏈表問題*/
            // Entry的next指向新數(shù)組的鏈表頭
            e.next = newTable[i];
            // Entry放入新數(shù)組的鏈表中
            newTable[i] = e;
            // 下一次進(jìn)行操作的就是原數(shù)組鏈表的下一個(gè)
            e = next;
        }
    }
}

擴(kuò)容步驟:

  • 首先傳入的newCapacity是數(shù)組容量的2倍,也是2的n次方
  • 如果數(shù)組的容量已經(jīng)達(dá)到2的30次方,則不進(jìn)行擴(kuò)容,直接返回
  • 用新的數(shù)組長度newCapacity創(chuàng)建Entry數(shù)組
  • initHashSeedAsNeeded判斷是否要重hash,并更新hash種子
  • transfer方法進(jìn)行擴(kuò)容處理

transfer方法進(jìn)行具體的擴(kuò)容處理:

實(shí)際上就是遍歷舊數(shù)組,從舊數(shù)組的第一個(gè)桶中拿到鏈表,從鏈表頭部開始遍歷,一個(gè)一個(gè)的取出計(jì)算新的下標(biāo)(如果需要重hash就會(huì)用新的hsah種子計(jì)算hsah值,如果不需要重hash就用原來的hsah值,最終用hsah值和新數(shù)組容量計(jì)算下標(biāo)),然后頭插法插到新的數(shù)組中。

你會(huì)發(fā)現(xiàn)兩個(gè)規(guī)律:

  • 原來數(shù)組中的鏈表和新數(shù)組中鏈表的順序相反。
  • 計(jì)算所得的新下標(biāo)要么等于原下標(biāo)值,要么等于原下標(biāo)加擴(kuò)容的長度。

我們通過一個(gè)例子來分析一下第二條規(guī)律。

假設(shè)table.length=16,現(xiàn)在有兩個(gè)key,key1對(duì)應(yīng)的hash值為68,key2對(duì)應(yīng)hash值為84,根據(jù)公式h & (length-1)計(jì)算,&運(yùn)算規(guī)則是遇0為0,結(jié)果如下:

68 key1 0100 0100 & 0000 1111  =0000 0100  =4 84 key2 0101 0100 & 0000 1111  =0000 0100  =4

可見兩個(gè)值都落在table[4]這個(gè)桶中。

擴(kuò)容一次后table.length=32,再根據(jù)公式h & (length-1)計(jì)算結(jié)果如下:

68 key1 0100 0100 & 0001 1111  =0000 0100  =4 84 key2 0101 0100 & 0001 1111  =0001 0100  =20

可見68依然被放在新數(shù)組的table[4],而84被放在table[20]。

再擴(kuò)容一次后table.length=64,再根據(jù)公式h & (length-1)計(jì)算結(jié)果如下:

68 key1 0100 0100 & 0011 1111  =0000 0100  =4 84 key2 0101 0100 & 0011 1111  =0001 0100  =20

可見兩個(gè)值還在原來的數(shù)組下標(biāo)對(duì)應(yīng)的桶中。

結(jié)論:同一個(gè)桶中的鏈表中的數(shù)據(jù),擴(kuò)容后,在新數(shù)組中的下標(biāo)要么和原數(shù)組相同,要么是原數(shù)組下標(biāo)加擴(kuò)容的長度。

這個(gè)規(guī)律是怎么形成的呢?

這得益于數(shù)組的容量都為2的n次方,數(shù)組每次擴(kuò)容的后結(jié)果都是原來數(shù)組容量的兩倍,例如:16,32,64...,length-1的結(jié)果分別是15,31,63,而對(duì)應(yīng)的二進(jìn)制如下:

0000 1111
0001 1111
0011 1111

可以看的出,每次擴(kuò)容都是高位加1,就導(dǎo)致在計(jì)算的時(shí)候只需要看hash值中與高位對(duì)應(yīng)的那個(gè)位上是0還是1,也就導(dǎo)致新數(shù)組中的下標(biāo)只有兩種可能:如果是0下標(biāo)不變,如果是1下標(biāo)就會(huì)變化。

這個(gè)規(guī)律有什么好處呢?

這個(gè)規(guī)律可以使原來在同個(gè)桶中的數(shù)據(jù)可以分散到其他桶中,使得數(shù)組分部更加均勻,減少hash沖突,擴(kuò)容的時(shí)候同個(gè)桶中的數(shù)據(jù)將會(huì)被分部到新數(shù)組中的哪個(gè)桶中,可以通過只判斷高位就能確定,所以可以以此來優(yōu)化擴(kuò)容效率。

6.頭插法

// createEntry方法
void createEntry(int hash, K key, V value, int bucketIndex) {
 // 獲取當(dāng)前數(shù)組下標(biāo)位置的鏈表頭
    Entry<K,V> e = table[bucketIndex];
    // 在鏈表頭位置創(chuàng)建新的Entry,next指向原鏈表頭(頭插法)
    table[bucketIndex] = new Entry<>(hash, key, value, e);
    // 數(shù)組長度+1
    size++;
}

頭插法的源碼就很簡單了,就是新建一個(gè)Entry對(duì)象,新建Entry對(duì)象的next屬性指向當(dāng)前坐標(biāo)下的頭部Entry對(duì)象,再把新的Entry對(duì)象引用賦值給當(dāng)前數(shù)組下標(biāo)處。

這里的文字說明可能比自己看源碼更繞。自己看源碼應(yīng)該一目了然。

五、問題匯總

1.為什么要計(jì)算得到大于等于指定數(shù)組長度的第一個(gè)2的n次方的值?

只有在數(shù)組長度為2的n次方時(shí),數(shù)組長度-1轉(zhuǎn)換為2進(jìn)制時(shí)才可以轉(zhuǎn)換為低位全部為1的二進(jìn)制,和hash值進(jìn)行&運(yùn)算時(shí)才能等效于hash值除以數(shù)組容量求余的結(jié)果。

2.為什么要用頭插法?

實(shí)際上無論是頭插法還是尾插法,都是需要遍歷鏈表的,如果在遍歷過程中找到key相同的,則更新覆蓋,這種情況不會(huì)有插入操作,所以無所謂頭插法和尾插法,但是如果沒有找到key相同的元素,那這時(shí)肯定已經(jīng)遍歷到鏈表的尾部了,所以但凡插入,不管是頭插法還是尾插法都不會(huì)在遍歷鏈表上節(jié)省時(shí)間,又因?yàn)殒湵淼牟迦雰H僅是更換next屬性的指針,所以兩種插入方式的效率是沒有區(qū)別的。

java1.7之所以使用頭插法應(yīng)該和自身的代碼結(jié)構(gòu)有關(guān),因?yàn)椴迦敕椒ㄊ仟?dú)立的,如果用尾插法,就要在遍歷的時(shí)候記錄最后一個(gè)元素的值,而頭插法就不需要了,但是我覺得這個(gè)不是主要原因,個(gè)人覺得java開發(fā)者只不過是兩者選擇了一個(gè)而已,沒有什么特殊考慮,否則也不至于會(huì)有循環(huán)鏈表的問題了。

3.hashMap為什么是線程不安全的?

hashMap線程不安全主要表現(xiàn)在兩個(gè)方面:

(1)多線程插入數(shù)據(jù)的時(shí)候,數(shù)據(jù)丟失問題

void createEntry(int hash, K key, V value, int bucketIndex) {
    Entry<K,V> e = table[bucketIndex];
    table[bucketIndex] = new Entry<>(hash, key, value, e);
    size++;
}

上面是頭插法的代碼邏輯,在多線程操作下,如果兩個(gè)線程同時(shí)走到方法內(nèi)第一行,那么獲取到的e是相同的,然后兩個(gè)線程分別創(chuàng)建Entry對(duì)象,并且Entry對(duì)象的next屬性賦值為e,這樣一來總會(huì)有一個(gè)線程的數(shù)據(jù)被丟掉了。

(2) 多線程情況下擴(kuò)容的時(shí)候可能會(huì)發(fā)生循環(huán)鏈表

循環(huán)鏈表發(fā)生在多線程擴(kuò)容的情況下,下面是擴(kuò)容的部分代碼:

for (Entry<K,V> e : table) {
    while(null != e) {
        Entry<K,V> next = e.next;
        if (rehash) {
          e.hash = null == e.key ? 0 : hash(e.key);
        }
        int i = indexFor(e.hash, newCapacity);
        e.next = newTable[i];
        newTable[i] = e;
        e = next;
    }

這段代碼邏輯是把舊數(shù)組中的數(shù)據(jù)從鏈表頭部一個(gè)個(gè)利用頭插法插入到新數(shù)組中。

假設(shè)有兩個(gè)線程同時(shí)進(jìn)行擴(kuò)容,兩條線程都執(zhí)行到下面這行代碼:

Entry<K,V> next = e.next;

此時(shí)第一條線程繼續(xù)執(zhí)行,第二條線程卡住,等到第一條線程整個(gè)循環(huán)執(zhí)行結(jié)束后,第二條線程才繼續(xù)執(zhí)行。

此時(shí)第一條線程擴(kuò)容完成,鏈表的指向和原數(shù)組的順序相反。假設(shè)原數(shù)組的某個(gè)桶中鏈表方向是1>2>3>4,擴(kuò)容后又恰好都落在同一個(gè)新的桶中,那么新的鏈表方向是4>3>2>1。

此時(shí)第二條線程開始執(zhí)行循環(huán):

  • 第一輪循環(huán)開始 e指的是1,next指的是2 頭插法插入新的數(shù)組,新數(shù)組的鏈表為1>null
  • 第二輪循環(huán)開始 e指的是2,next指的是1 頭插法插入新的數(shù)組,新數(shù)組的鏈表為2>1>null
  • 第三輪循環(huán)開始 e指的是1,next指的是null 頭插法插入新的數(shù)組,新數(shù)組的鏈表為1>2>1

此時(shí)循環(huán)鏈表出現(xiàn)了。

責(zé)任編輯:趙寧寧 來源: 碼農(nóng)本農(nóng)
相關(guān)推薦

2023-01-04 07:54:03

HashMap底層JDK

2021-10-11 11:58:41

Channel原理recvq

2023-05-29 08:12:38

2021-10-09 19:05:06

channelGo原理

2023-07-11 08:00:00

2021-07-08 10:08:03

DvaJS前端Dva

2022-04-24 11:06:54

SpringBootjar代碼

2022-08-26 13:24:03

version源碼sources

2021-07-16 11:35:20

Java線程池代碼

2021-08-29 07:41:48

數(shù)據(jù)HashMap底層

2025-04-21 04:00:00

2020-04-28 22:12:30

Nginx正向代理反向代理

2021-09-22 07:58:51

Linux 系統(tǒng)調(diào)用Linux 系統(tǒng)

2017-12-05 17:44:31

機(jī)器學(xué)習(xí)CNN卷積層

2020-10-14 08:50:38

搞懂 Netty 線程

2021-08-18 23:10:56

setState代碼性能

2024-01-03 13:39:00

JS,Javascrip算法

2025-01-13 16:00:00

服務(wù)網(wǎng)關(guān)分布式系統(tǒng)架構(gòu)

2025-04-11 05:55:00

2019-07-23 08:55:46

Base64編碼底層
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)