并发下诡异的HashMap

最近研读《Java高并发程序设计》葛一鸣、郭超编著,读到2.8.3时,题目便是并发下诡异的HashMap,摘抄如下:

-----------摘抄开始--------------

HashMap同样不是线程安全的。当你使用多线程访问HashMap时,也可能会遇到意想不到的错误。不过和ArrayList不同,HashMap的问题似乎更加诡异。

package cn.baokx;

import java.util.HashMap;
import java.util.Map;

public class HashMapMultiThread {
	static Map<String,String> map = new HashMap<String,String>();
	public static class AddThread implements Runnable{
		int start = 0 ;
		public AddThread(int start){
			this.start = start;
		}
		@Override
		public void run() {
			for (int i = 0; i < 100000; i+=2) {
				map.put(Integer.toString(i), Integer.toBinaryString(i));

			}
		}
	}

	public static void main(String[] args) throws InterruptedException {

		Thread t1 = new Thread(new HashMapMultiThread.AddThread(0));
		Thread t2 = new Thread(new HashMapMultiThread.AddThread(1));
		t1.start();
		t2.start();
		t1.join();
		t2.join();
		System.out.println(map.size());
	}
}

上述代码使用t1和t2两个线程同时对HashMap进行put()操作,如果一切正常,我们期望得到的map.size()就是100000.但实际上,你可能会得到以下三种情况(注意,这里使用JDK7进行试验):

第一:程序正常结束,并且结果也是符合预期的。HashMap的大小为100000.

第二:程序正常结束,但结果不符合预期,而是一个小于100000的数字,比如98868.

第三:程序永远无法结束。

对于前两种可能,和ArrayList的情况非常类似,因此,不必过多解释。而对于第三种情况,如果是第一次看到,我想大家一定会觉得特别惊讶,因为看似非常正常的程序,怎么可能就结束不了呢?

注意:请读者谨慎尝试以上代码,由于这段代码很可能占用两个CPU核,并使它们的CPU占有率达到100%。如果CPU性能较弱,可能导致死机。请先保存资料再进行尝试。

打开任务管理器,你会发现,这段代码占用了极高的CPU,最有可能的表示是占用了两个CPU核,并使得这两个核的CPU使用率达到100%。这非常类似死循环的情况。

使用jstack工具显示程序的线程信息,如下所示。其中jps可以显示当前系统中所有的java进程。二jstack可以打印给定的java进程的内部线程及其堆栈。

我们会很容易找到我们的t1、t2和main线程:

可以看到,主线程main处于等待状态,并且这个等待是由于join方法引起的,符合我们的预期,二t1和t2两个线程都处于Runnable状态,并且当前执行语句为HashMap.put()方法。查看put()方法的第498行代码,如下所示:

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

可以看到,当前这两个线程正在遍历HashMap的内部数据。当前所处循环乍看之下是一个迭代遍历,就如同遍历一个链表一样。但在此时此刻,由于多线程的冲突,这个链表的结构已经遭到破坏,链表成环了!当链表成环时,上述的迭代就等同于一个死循环,如图2.9所示,展示了最简单的一种环状结构,Key1和Key2互为对方的next元素。此时,通过next引用遍历,将形成死循环。

这个死循环的问题,如果一旦发生,着实可以让你郁闷一把。本章的参考资料中也给出了一个真实的按理。但这个死循环的问题在JDK8中已经不存在了。由于JDK8对HashMap的内部做了大规模的调整,一次规避了这个问题。但即使这样,贸然在多线程环境下使用HashMap一人会导致内部数据不一致。最简单的解决方案就是使用ConcurrentHashMap代替HashMap。

-----------摘抄结束--------------

以上内容为书上内容的摘抄,看完以后对红色标注的部分"但在此时此刻,由于多线程的冲突,这个链表的结构已经遭到破坏,链表成环了!"无法理解,到底是什么情况下导致链表成环了呢?书中并没有展开,通过自己最近几天的查询和学习,确认了链表成环的原因,在此分享出来。

HashMap内部通过一个Entry<K,V>[]
table来存储数据,当调用put方法时,根据key的hashcode进行Hash计算,得出一个数组下标,然后将Entry对象放至相应位置,在这种情况下可能发生不同的hashcode进行Hash计算得到的下标相同的情况,这种情况下,会将Entry进行链式存储(Entry内部本身定义了Entry类型的next属性,据此实现链式存储),和ArrayList一样,table的长度会有一个初始值(HashMap默认为16)和load_factor(默认0.75f),当数组的“使用率(即不为空的元素数量/长度)”达到load_factor时,就需要对数组进行扩容,扩容的时候需要定义一个新的数组,并把旧的数组中的Entry分配到新的数组中。

对于HashMap内部的存储方式,可参考博客,http://blog.csdn.net/baokx/article/details/51426899,里面讲的比较详细。

源代码分析如下所示:

    //注意put方法是有返回值的
    public V put(K key, V value) {
        if (table == EMPTY_TABLE) {
            inflateTable(threshold);
        }
        //HashMap允许有一个key为null的键值对
        if (key == null)
            return putForNullKey(value);
        int hash = hash(key);
        //计算下标
        int i = indexFor(hash, table.length);
        //循环下标处的Entry链表
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
            Object k;
            //hash值相等且key==或equals匹配,则替换value值并把旧值返回
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }

        modCount++;
        //若循环Entry链表未匹配到,则加一个Entry
        addEntry(hash, key, value, i);
        return null;
    }

    //新增一个Entry
    void addEntry(int hash, K key, V value, int bucketIndex) {
    	//若满足条件则需要扩容
        if ((size >= threshold) && (null != table[bucketIndex])) {
            resize(2 * table.length);
            hash = (null != key) ? hash(key) : 0;
            bucketIndex = indexFor(hash, table.length);
        }

        createEntry(hash, key, value, bucketIndex);
    }

    //扩容
    void resize(int newCapacity) {
        Entry[] oldTable = table;
        int oldCapacity = oldTable.length;
        if (oldCapacity == MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return;
        }

        //创建一个容量更大的新的数组
        Entry[] newTable = new Entry[newCapacity];
        //将旧数组中的Entry数据转移至新的数组
        transfer(newTable, initHashSeedAsNeeded(newCapacity));
        table = newTable;
        threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
    }

    //转移数据
    void transfer(Entry[] newTable, boolean rehash) {
        int newCapacity = newTable.length;
        for (Entry<K,V> e : table) {
            while(null != e) {
                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;
            }
        }
    }

注意:上面的transfer方法,在多线程的情况下,会出现newTable[i].next=newTable[i]的情况,这也是文中提到的链表成环的原因。

为了验证我们的判断,在创建数组的时候未数组提供初始化容量和影响因子,目的是不让扩容的情况发生,代码如下:

public class HashMapMultiThread {
	static Map<String,String> map = new HashMap<String,String>(120000,1.0f);
	public static class AddThread implements Runnable{
		int start = 0 ;
		public AddThread(int start){
			this.start = start;
		}
		@Override
		public void run() {
			for (int i = 0; i < 100000; i+=2) {
				map.put(Integer.toString(i), Integer.toBinaryString(i));

			}
		}
	}

	public static void main(String[] args) throws InterruptedException {

		Thread t1 = new Thread(new HashMapMultiThread.AddThread(0));
		Thread t2 = new Thread(new HashMapMultiThread.AddThread(1));
		t1.start();
		t2.start();
		t1.join();
		t2.join();
		System.out.println(map.size());
	}
}

通过验证,不再出现死循环的问题(若觉得这样修改仍不能确定是由于代码修改导致不再死循环,可以增加线程数,再对比修改前后的运行结果)。

以上就是对此疑问的分析。

时间: 2024-10-15 15:24:58

并发下诡异的HashMap的相关文章

2.8.3 并发下诡异的HashMap

package 第二章.并发下诡异的HashMap; import org.junit.Test; import java.util.HashMap;import java.util.Map;import java.util.concurrent.atomic.AtomicInteger; /** * Created by zzq on 2018/1/19. */public class HashMapMultiThread { static HashMap<String, String> m

2 java并行基础

我们认真研究如何才能构建一个正确.健壮并且高效的并行系统. 进程与线程 进程(Process):是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础. 进程是线程的容器.程序是指令.数据和其组织形式的描述,进程是程序的实体.进程中可以容纳若干个线程. 进程和线程的关系:线程就是轻量级的进程,是程序执行的最小单位.为什么我们使用多线程而不是多进程?因为线程间的切换调度成本远远小于进程,所以我们使用多线程而不是多进程. 线程的生命周期 线程的所有状

HhashMap HashTable ConcurrentHashMap

hashMap hashTable concurrentHashMap hashMap的效率高于hashTable,hashMap是线程不安全的,并发时hashMap put方法容易引起死循环,导致cpu利用率达到100% 所以高并发下不能使用hashMap,而是用ConcurentHashMap,则不会出现这种情况

19.并发下的ArrayList、HashMap,Integer加锁问题

import java.util.ArrayList; import java.util.Vector; /** * 并发下的ArrayList */ public class ArrayListMultiThread { static ArrayList<Integer> arrayList = new ArrayList<>(); // static Vector<Integer> arrayList = new Vector<>(); //解决方式 使

HashMap在高并发下引起的死循环

HashMap其实并不是线程安全的,在高并发的情况下,是很可能发生死循环的,由此造成CPU 100%,这是很可怕的,所以在多线程的情况下,用HashMap是很不妥当的行为,应采用线程安全类ConcurrentHashMap进行代替. HashMap死循环原因 HashMap进行存储时,如果size超过当前最大容量*负载因子时候会发生resize,首先看一下resize原代码 void resize(int newCapacity) { Entry[] oldTable = table; int

HashMap在高并发下如果没有处理线程安全会有怎样的安全隐患,具体表现是什么

Hashmap在并发环境下,可能出现的问题: 1.多线程put时可能会导致get无限循环,具体表现为CPU使用率100%: 原因:在向HashMap put元素时,会检查HashMap的容量是否足够,如果不足,则会新建一个比原来容量大两倍的Hash表,然后把数组从老的Hash表中迁移到新的Hash表中,迁移的过程就是一个rehash()的过程,多个线程同时操作就有可能会形成循环链表,所以在使用get()时,就会出现Infinite Loop的情况 // tranfer()片段 // 这是在res

漫画:高并发下的HashMap

这一期我们来讲解高并发环境下,HashMap可能出现的致命问题. HashMap的容量是有限的.当经过多次元素插入,使得HashMap达到一定饱和度时,Key映射位置发生冲突的几率会逐渐提高. 这时候,HashMap需要扩展它的长度,也就是进行Resize. 影响发生Resize的因素有两个:   1.Capacity HashMap的当前长度.上一期曾经说过,HashMap的长度是2的幂. 2.LoadFactor HashMap负载因子,默认值为0.75f. 衡量HashMap是否进行Res

高并发下,HashMap会产生哪些问题?

HashMap在高并发环境下会产生的问题 HashMap其实并不是线程安全的,在高并发的情况下,会产生并发引起的问题: 比如: HashMap死循环,造成CPU100%负载 触发fail-fast 下面逐个分析下出现上述情况的原因: HashMap死循环的原因 HashMap进行存储时,如果size超过(当前最大容量*负载因子)时候会发生resize,首先看一下resize源代码: void resize(int newCapacity) { Entry[] oldTable = table;

HashMap并发下死循环问题解析

首先小伙伴要明确:死循环问题在JDK 1.8 之前是存在的,JDK 1.8 通过增加loHead和loTail进行了修复. 在JDK 1.7及之前 HashMap在并发情况下导致循环问题,致使服务器cpu飙升至100%,那么今天就来解析一下线程不安全的HashMap在高并发的情况下是如何造成死循环的. 要探究hashmap死循环的原因 首先要知道hashmap的源码 这样才能从根本上对hashmap进行理解 . 首先hashmap进行元素的插入,在元素个数达到阀值时: 首先小伙伴要明确:死循环问