文章目录
1. 预备知识
扩容产生的条件:
- 如果HashMap初始化的时候,没有设置初始容量,那么初始容量默认为16,扩容阈值threshold为16*0.75f=12;
- 如果设置了初始容量,那么首次扩容阈值threshold为初始容量。
HashMap的插入
- JDK1.7的插入是头插法
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor;
threshold = initialCapacity;// 👈在这里设置了扩容阈值,在构造函数中设置容量,阈值便会与与容量保持一致(第一次扩容之前)
init();
}
那么在添加Entry的时候,会进行判断
void addEntry(int hash, K key, V value, int bucketIndex) {
if ((size >= threshold) && (null != table[bucketIndex])) {//size>=threshold,并且需要插入的位置不为空,可能出现扩容
resize(2 * table.length);
hash = (null != key) ? hash(key) : 0;
bucketIndex = indexFor(hash, table.length);
}
createEntry(hash, key, value, bucketIndex);
}
addEntry->resize->transfer
void resize(int newCapacity) {
Entry[] oldTable = table;
int oldCapacity = oldTable.length;
if (oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return;
}
Entry[] newTable = new Entry[newCapacity];
transfer(newTable, initHashSeedAsNeeded(newCapacity));
table = newTable;
threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}
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) {
e.hash = null == e.key ? 0 : hash(e.key);
}
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
e = next;
}
}
}
2. 关于成环
这里为了表示方便,我们设置初始容量为8,那么必须当size达到8的时候,再往里添加键值对才可能出现扩容现象。
我们使用的键值以及其在长度8和长度16的情况下对应的位置如下
Han1250--->len=8:1---- len=16:1
Han1251--->len=8:2---- len=16:2
Han1252--->len=8:3---- len=16:3
Han1253--->len=8:4---- len=16:4
Han1254--->len=8:5---- len=16:5
Han1255--->len=8:6---- len=16:6
ZH1252222--->len=8:6---- len=16:6
ZH125--->len=8:6---- len=16:6
那么在长度为8时,数据的排布情况为:
接下来我们默认以蓝色数组代表线程A创建的newTableA
,绿色数组代表线程B创建的newTableB
,新数组的长度都为原来的二倍,初始长度为8,旧数组命名为oldTble
,扩容后长度为16。
2.1 线程A
在线程A中将前五个元素插入
接下来进入重点
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) {
e.hash = null == e.key ? 0 : hash(e.key);
}
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];//②
newTable[i] = e;//③
e = next;//④
}
}
}
线程A运行到位置6的Entry时,e
指向ZH125
,next
指向ZH1252222
,假设此时线程A失去了CPU,线程B得到了CPU。我们称之为剥夺
。
2.2 线程B
我们略过前几步。
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;//④
2.3 分析一下堆中的情况
OldTable
作为底层数组,与HashMap对象生命周期相同,也是在堆中开辟的空间,无论多少个线程,都是操作的这一个对象,如果不加锁进行限制,则会产生数据的错乱。
在经过了上面的操作之后,堆中的数据情况如下:

线程A和B中的e
和next
的指向如下图:

2.4 我们重新转入线程A
为社么会产生这种情况呢?由于链表的插入为头插法,我们设一个链表上的两个Entry为E1
和E2
,假设我们在线程A中,e和next分别是E1
和E2
的引用,这时候线程B抢夺到了CPU,E1
和E2
在新的Table数组中的位置依然相同,仍旧在同一链表中,在oldTableA
中两个Entry的顺序为E1->E2
,经过线程B的操作,顺序变成了E2->E1
,
这时候我们回到线程A中
e:E1
nex:E2
将e设置到newTableA的i
处,newTableA[i]=e; e.next=newTableA[i];
接下来,e=next;
,e指向了E2,while进入了下一轮循环。
进入新的循环,next=e.next;
,此时e指向E2,E2.next==e.next==E1
我们继续将e头插入新的链表,e=next;next=e.next;
此时我们发现,如果将e
继续头插入新链表,ZH125.next==ZH1252222
,而ZH1252222.next=ZH125
;
2.5 动图
3. 分析Entry丢失的情况
3.1 前提信息
3.2 线程B
3.3 线程A
3.4 线程B