其他分享
首页 > 其他分享> > HashMap中扩容导致环链现象分析

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


数据分析

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