ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

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

2021-07-16 21:00:22  阅读:249  来源: 互联网

标签:扩容 HashMap newTable next 环链 线程 new Entry


目录

为什么会出现环链?

什么时候会出现环链?

出现环链的场景:

      数据准备、结果预期

      模拟的转移代码

  多线程的场景分析

 形成的根本原因

数据分析

线程一数据变化

第一次循环

 第二次循环

线程二开始执行


为什么会出现环链?

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


数据分析

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

 

标签:扩容,HashMap,newTable,next,环链,线程,new,Entry
来源: https://blog.csdn.net/qq_38023253/article/details/118768438

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有