同花顺
这两个不同之处,在源码上都可以体现出来。
第一点不同,非常容易理解。
源码中,首先获取旧的数组,之后遍历。
遍历的第一个就是A,之后将A放到新数组的Index位上。
遍历的第二个是B,之后将B放到新数组的Index位上。
慢慢的A就被挤到最下边了,形成了顺序倒置。
这个是很好理解的,比较难以理解的是第二个不同。
环形链表以及死循环是如何产生的。
1、指针顺序不同
头插法与尾插法和栈与队列非常相似。
栈是先进后出,队列是先进先出。
这种比喻其实是不太形象的,糊涂般的这么理解一下即可。
2、头插法导致环形链表
这种问题的产生,归根结底还是线程安全问题,没有保证原子性,正在执行的线程受到了其他线程或其他因素的干扰。
在解释的过程中,我采取源码+图解+注释的方式解释说明,环形链表是如何产生的。
场景如下:
线程T1与线程T2共同put一个hashmap,同时触发了扩容机制。
在整个过程中,线程T1是真正正儿八经干活的,线程T2是来捣蛋的。
线程T2严重影响了线程T1的执行,导致死循环,罪魁祸首。
首先,看一下源码,简单介绍一下之后,再一步一步的走一遍源码。
一共分为四部分源码,从put一步一步走下去,重点是最后一步,前三部分可以直接忽略。
public V put(K key, V value)
{
......
int hash = hash(key.hashCode());
int i = indexFor(hash, table.length);
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
//----------------------------增加节点。
addEntry(hash, key, value, i);
return null;
}
void addEntry(int hash, K key, V value, int bucketIndex)
{
Entry<K,V> e = table[bucketIndex];
table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
if (size++ >= threshold)
//----------------------------判断是否满足扩容条件,满足则扩容。
resize(2 * table.length);
}
void resize(int newCapacity)
{
Entry[] oldTable = table;
int oldCapacity = oldTable.length;
//----------------------------创建新数组
Entry[] newTable = new Entry[newCapacity];
//----------------------------旧数组数据迁移到新数组。
transfer(newTable);
table = newTable;
threshold = (int)(newCapacity * loadFactor);
}
void transfer(Entry[] newTable, boolean rehash) {
//新数组的容量,扩容后每一个桶的位置是会发生改变的。
//具体的Index有一个公式决定,其中一个决定因素为新数组容量大小。
int newCapacity = newTable.length;
//遍历旧数组
for (Entry<K,V> e : table) {
//数组中每一个元素下边为链表,直到链表中每一个元素都遍历后,下一个节点next为空时,跳出循环,进入数组中的下一个元素桶的遍历。
while(null != e) {
//旧数组中的当前元素的下一个元素。
Entry<K,V> next = e.next;
if (rehash) {
e.hash = null == e.key ? 0 : hash(e.key);
}
//判断扩容后的桶的Index。
int i = indexFor(e.hash, newCapacity);
//当前元素指向新数组的index位number1置。
e.next = newTable[i];
//把旧数组的这个元素,乾坤大挪移到新数组。
newTable[i] = e;
//让旧数组的这个元素的下一个元素作为下次循环的主角,直到旧数组的这个元素的下一个元素为空,这个桶就算是遍历完了,遍历下一个桶去吧。
e = next;
}
}
}
3、死循环的产生
线程在运行过程中,争取CPU的执行权,就会导致线程T1竞争失败,由T2线程执行完毕后,T1才会执行。
死循环的产生不是每次碰到扩容,每次碰到多线程扩容就会产生,是凑巧行为。
第一次循环
T1挂起,T2执行完毕
void transfer(Entry[] newTable, boolean rehash) {
int newCapacity = newTable.length;
for (Entry<K,V> e : table) {
while(null != e) {
//线程T1执行到这里,挂起,T1记录数据为e=A,next=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;
}
}
//线程T2已经完成了旧数组到新数组的迁移,所有的遍历循环执行完毕,进入死亡状态。
}
T1恢复运行状态
void transfer(Entry[] newTable, boolean rehash) {
int newCapacity = newTable.length;
for (Entry<K,V> e : table) {
while(null != e) {
//线程T1执行到这里,挂起,T1记录数据为e=A,next=B。
Entry<K,V> next = e.next;
if (rehash) {
e.hash = null == e.key ? 0 : hash(e.key);
}
//经过计算,Index位于T2计算的Index位一致,都是1。
int i = indexFor(e.hash, newCapacity);
//这里已经受到了T2的影响,新数组的1号位上已经有数据了。真正导致死循环的也是这一行代码,指针互指。旧数组中的e(A)指向新数组上1号位,建立联系。
e.next = newTable[i];
//将旧数组中的A元素放到新数组的1号位上。
newTable[i] = e;
//将next的值赋给e,开启下一轮循环,此时e=B。
e = next;
}
}
}
第二次循环
void transfer(Entry[] newTable, boolean rehash) {
int newCapacity = newTable.length;
for (Entry<K,V> e : table) {
while(null != e) {
//接上次循环的数据e为B,这里next是谁。
//在第一次循环时,T1将这行代码执行完挂起使用的是旧数组中的数据,所以它的第一次循环也是旧数组的,但是此时,T2已经执行完毕,再次受到了T2线程的影响,B.next=A,所以next=A。
//最简单的理解方式为旧数组是一次性筷子,已经被T2用过了,没了,数据B的next在已有数据中只能为A。
Entry<K,V> next = e.next;
if (rehash) {
e.hash = null == e.key ? 0 : hash(e.key);
}
int i = indexFor(e.hash, newCapacity);
//让B指向A,下边再头插赋值。
e.next = newTable[i];
//使用头插法将e=B,插入新数组的头部。
newTable[i] = e;
//将next复制给e,开始下一轮循环,此时e=A。
e = next;
}
}
}
第三次循环
问题的出现就是在第三次循环中。
void transfer(Entry[] newTable, boolean rehash) {
int newCapacity = newTable.length;
for (Entry<K,V> e : table) {
while(null != e) {
//接上一次循环,e为A,看上图得出,next = null = A.next;
Entry<K,V> next = e.next;
if (rehash) {
e.hash = null == e.key ? 0 : hash(e.key);
}
int i = indexFor(e.hash, newCapacity);
//这里让A指向了新数组的1号位B。
//但是现在B已经指向A了,又让A指向B是什么鬼?
//A.next=B; B.next=A,环形链表出现。
e.next = newTable[i];
newTable[i] = e;
e = next;
}
}
}
4、总结
两个线程,线程A,线程B。
在并发的时候原来的顺序被另外一个线程a颠倒了,而被挂起线程b恢复后拿扩容前的节点和顺序继续完成第一次循环后。
又遵循a线程扩容后的链表重新排列链表中的顺序,最终形成了环。