徹底搞懂hashMap底層原理
一、說明
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方法
步驟:
- 判斷table是否為空,如果為空則初始化,不為空則下一步
- 判斷key是否為null,如果為null則處理,不為null則下一步
- 根據(jù)key計(jì)算下標(biāo)
- 如果下標(biāo)處的桶不為空,則從下標(biāo)處開始遍歷鏈表,如果找到key相同的則更新覆蓋并返回舊值,如果為空則下一步
- 插入前判斷是否需要擴(kuò)容,如果需要?jiǎng)t進(jìn)行擴(kuò)容,如果不需要就下一步
- 頭插法插入桶中
接下來我們對(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)了。