目录
为什么会出现环链?
HashMap是线程不安全的,并且其扩容的代码会将原来的链表进行反序,例如原先的是 3-> 2-> 1,现在还在同一位置则是 1->2->3,那么在多线程并发操作的情况下一个正序,一个反序,就会有可能出现。
什么时候会出现环链?
多线程操作,同时去扩容,当一个线程已经操作完毕了,将原来的顺序反过来了;另一个线程再开始执行扩容代码,此时就会出现环链。
正常的扩容转移效果如下:
代码运行的结果如下:
出现环链的场景:
数据准备、结果预期
首先初始化一个数组,数组的一个元素是一个链表,有三个对象,模拟扩容的操作,将其中的一个分散到新数组的一个槽位中,将剩余的两个仍形成一个链表的结构。
模拟的转移代码
散列的函数粗糙点,主要是为了达到两个元素存在于一个槽位的目的
public static void transfer(Entry[] newTable, Entry[] table) {
for (Entry e : table) {
while (e != null) {
//1 、拿出下一个以及后面的所有
Entry next = e.next;
// 散列效果看i的分布
int i = Integer.parseInt(e.key) > 1 ? 1 : 0;
// 2、断开e与之前的,指向新的;新的可能是空,也可能有对象 ,有对象的话就是头插法
e.next = newTable[i];
// 3、再把E新操作后的一串对象赋值给新的数组。 2/3这两步实现头插转移
newTable[i] = e;
//4、指针移动到下一个,直到结束
e = next;
}
}
}
正常结果如下:
多线程的场景分析
将一个线程先暂停住,让另一个线程全部执行完,再开始执行暂停的那个线程
public static void main(String[] args) throws InterruptedException {
//初始化数据
Entry entry1 = new Entry<String, String>();
entry1.setKey("3");
entry1.setValue("A");
Entry entry2 = new Entry<String, String>();
entry2.setKey("2");
entry2.setValue("B");
Entry entry3 = new Entry<String, String>();
entry3.setKey("1");
entry3.setValue("c");
entry1.next = entry2;
entry2.next= entry3;
//构造table数据 以及新的扩容后的newTable
Entry[] table = new Entry[1];
table[0] = entry1;
Entry[] newTable = new Entry[5];
//保证两个线程都结束后,再查看主线程的newTable 数据
final CountDownLatch cdl = new CountDownLatch(2);
Thread t1 = new Thread() {
public void run() {
transfer(newTable, table);
//执行结束后计数减1
cdl.countDown();
}
};
Thread t2 = new Thread() {
public void run() {
System.out.println("线程2开始执行");
transfer(newTable, table);
//执行结束后计数减1
cdl.countDown();
}
};
t1.setName("线程1");
t2.setName("线程2");
t1.start();
t2.start();
try {
//线程等待,当计数为0时,可继续执行
cdl.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("两个线程都执行完毕,继续下一步");
System.out.println(Arrays.asList(newTable));
}
当线程二再继续的时候,newTable已经转移好了,此时线程2的e是原来的第一个元素3
执行完代码的第2步 将当前的E,新数组单链的尾节点 指向单链的头节点,这时环链就形成了,第3步就是将新的e赋值给新的数组对象
最终结束后,其中形成的环链效果如下所示。
形成的根本原因
在使用头插法转移对象时,同样的处理逻辑在不同线程中执行时,当一个线程的已经结束,此时是一根单链条,从头到尾结尾,越早进来的就在靠近尾节点处。
若此时另一个线程的首元素即新数组的尾节点,还刚开始执行。此时将这个尾节点的next再指向于这整个单链条的首节点,这时候就出现了环链现象。
对共享资源的操作,在并发的情况下,就会出现以上现象。我们使用时候要注意,以上代码基于JAVA1.6进行的分析。
数据分析
初始化一个需要扩容转移的数组数据,如下所示,有三个元素组成。为了转移后的数据演示,将其中两个元素转移到同一个位置上, key 为1的元素单独一个位置。
线程一数据变化
第一次循环
第一步 找到e的后面一串元素并保存下
Entry next = e.next;
第二步 将e的下一个指向新的数组元素,此时新数组元素为空;
e.next = newTable[i];
i的散列效果,在这里只做一个简单的处理,保证有两个元素在同一位置就好,与实际的hash效果有区别,还望注意。
第三步 将新的e 赋值给new Table[i]
newTable[i] = e;
第四步 移动e到下一个元素
e = next;
至此,一次循环完成,第一个元素成功放入了new Table[i] 中。
第二次循环
按一次循环的四步操作,其中第二步的指向新数组,新数组此时已有一个元素,这时采用头插法会将新元素插入进去。此时第二次的四步操作之后数据如下所示。
线程二开始执行
此时的线程2开始执行,现在指向的的数组一直有两个元素了,其中的第一个元素是2,第二个元素是3.
当执行 e.next = newTable[i]; 此时就形成了环链。