HashMap中扩容导致环链现象分析

目录

为什么会出现环链?

什么时候会出现环链?

出现环链的场景:

      数据准备、结果预期

      模拟的转移代码

  多线程的场景分析

 形成的根本原因

数据分析

线程一数据变化

第一次循环

 第二次循环

线程二开始执行


为什么会出现环链?

       HashMap是线程不安全的,并且其扩容的代码会将原来的链表进行反序,例如原先的是 3-> 2-> 1,现在还在同一位置则是 1->2->3,那么在多线程并发操作的情况下一个正序,一个反序,就会有可能出现。

什么时候会出现环链?

      多线程操作,同时去扩容,当一个线程已经操作完毕了,将原来的顺序反过来了;另一个线程再开始执行扩容代码,此时就会出现环链。

 正常的扩容转移效果如下:

代码运行的结果如下: 

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

正常结果如下:

其中的1单独存在,元素2/3一起存放,并且2->3

  多线程的场景分析

将一个线程先暂停住,让另一个线程全部执行完,再开始执行暂停的那个线程

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进行的分析。

    在java1.8中 resize()方法在扩容转移时,优化成了尾插法,修复了环链这个问题。但在并发环境中仍是不安全的。


 

数据分析

        初始化一个需要扩容转移的数组数据,如下所示,有三个元素组成。为了转移后的数据演示,将其中两个元素转移到同一个位置上, 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]; 此时就形成了环链。

原文链接:https://blog.csdn.net/qq_38023253/article/details/118768438
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值