精华内容
下载资源
问答
  • HashMap为什么是线程不安全的?如何解决HashMap线程不安全问题?
  • 基于JDK1.7与JDK1.8HashMap线程安全问题 前言 通过学习HashMap,我们可以总结:HashMap是线程安全的,而HashTable是线程安全的。那么,为何HashMap是线程安全的呢?本篇文章通过JDK1.7和JDK1.8分别讲解为何...

    基于JDK1.7与JDK1.8HashMap线程安全问题

    前言
    通过学习HashMap,我们可以总结:HashMap是线程不安全的,而HashTable是线程安全的。那么,为何HashMap是线程不安全的呢?本篇文章通过JDK1.7和JDK1.8分别讲解为何HashMap是线程不安全的。

    一.JDK1.7分析HashMap线程不安全

    首先,先来看一下JDK1.7中HashMap的相关源码,只有通过源码,我们才能找出其中线程不安全的地方。
    1.底层数据结构只数组:

    //空的存储实体  
    transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;
    

    通过JDK1.7HashMap的源码可知,HashMap底层是使用了Entry类型的数组来存储变量。其中的transient表示HashMap是不需要序列化标识。
    Entry是HashMap的一个内部类,里面存储的是一个键值对,如,Entry<K,V>。因此这是一个存储键值对的类型的数组。

    2.Entry<K,V>源码:

    static class Entry<K,V> implements Map.Entry<K,V> {
            final K key;
            V value;
            Entry<K,V> next;//存储指向下一个Entry的引用,单链表结构
            int hash;//对key的hashcode值进行hash运算后得到的值,存储在Entry,避免重复计算
     }
    

    通过源码分析,HashMap除了有数组外,还有Entry类型的链表,Entry<K,V> next,每一个Entry块是一个节点,数据结构如下:
    在这里插入图片描述
    JDK1.7中HashMap的数据结构:数组+链表

    那么为何HashMap使用这样的数据结构呢?

    首先,我们知道数组在数据查询的效率上比链表快,而链表在增删元素的效率上比数组快,所以HashMap整合了这两种数据结构,数组用于存放key在散列表上的索引(位置),链表用来存放元素的具体空间。

    注意
    HashMap规定put进去的key值是不同的,而通过计算key的hashcode,可能出现相同的值。因此,不同的key通过hashcoed计算可能出现相同的索引,也就是散列表上相同的位置。这时,我们就可以使用链表存储这些元素,如果一个key只存放在一个数组中,这是最理想的;如果,同一个数组(索引位置)出现了多个相同的key,那么,我们就将这些元素存在同一个数组后对应的Entry中,每出现一个相同的key,就添加一个新节点,逐渐形成了像链表一样的存储空间。

    为何数组查询效率比链表快,而增删比链表慢?
    查询:
    数组中,数据在内存中是连续的,成块的。我们可以根据数组的首地址+偏移量(数组下标)直接计算对应位置的元素,所以查询快
    链表中,数据在内存中不是连续的一段空间,它的结构是「元素|下一个元素地址],当我们想要查找对应位置的元素时,它只能从首元素开始,依次获取下一个元素的地址,所以查询慢。
    增删:
    数组:由于数据在内存中是连续的,就要移动对应元素后面的所有元素,即,每增删一次,所有对应位置后面的元素都需要向前或向后移动,因此增删效率低。
    链表:在添加元素时,只要将此元素位置的前一元素和后一元素关联到此元素,删除元素时,只要把要删除元素的前一元素和后一元素的关联断掉即可,不会影响其他元素,因此增删效率快。

    3.put源码:

    public V put(K key, V value) {
            //如果table数组为空数组{},进行数组填充(为table分配实际内存空间),入参为threshold,此时threshold为initialCapacity 默认是1<<4(=16)
            if (table == EMPTY_TABLE) {
                inflateTable(threshold);//分配数组空间
            }
           //如果key为null,存储位置为table[0]或table[0]的冲突链上
            if (key == null)
                return putForNullKey(value);
            int hash = hash(key);//对key的hashcode进一步计算,确保散列均匀
            int i = indexFor(hash, table.length);//获取在table中的实际位置
            for (Entry<K,V> e = table[i]; e != null; e = e.next) {
            //如果该对应数据已存在,执行覆盖操作。用新value替换旧value,并返回旧value
                Object k;
                if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                    V oldValue = e.value;
                    e.value = value;
                    e.recordAccess(this);//调用value的回调函数,其实这个函数也为空实现
                    return oldValue;
                }
            }
            modCount++;//保证并发访问时,若HashMap内部结构发生变化,快速响应失败
            addEntry(hash, key, value, i);//新增一个entry
            return null;
        }
    

    此步骤可能出现线程不安全

    假设有两个线程A和B,当线程A执行int i = indexFor(hash, table.length)时,获取了当前元素在数组的位置,而此时B也执行了此方法,也获取了当前元素在数组的位置,如果线程A和线程B都得到同一个数组位置,那么线程B的元素就覆盖了线程A的元素在数组的位置,造成安全隐患,所以线程不安全。

    4.put中的addEntry源码:

    void addEntry(int hash, K key, V value, int bucketIndex) {
            if ((size >= threshold) && (null != table[bucketIndex])) {
                resize(2 * table.length);//当size超过临界阈值threshold,并且即将发生哈希冲突时进行扩容,新容量为旧容量的2倍
                hash = (null != key) ? hash(key) : 0;
                bucketIndex = indexFor(hash, table.length);//扩容后重新计算插入的位置下标
            }
    
            //把元素放入HashMap的桶的对应位置
           createEntry(hash, key, value, bucketIndex);
     }
    

    此步骤可能引起线程不安全

    现在假设有线程A和线程B,同时对同一个数组位置调用addEntry,那么,这两个线程可能同时得到头结点,当线程A写入新的头结点时,线程B也写入新的头结点,那么线程B的写入操作就覆盖了线程A的写入操作,这会导致A线程的写入操作丢失,造成线程不安全。

    5.addEntry中的resize源码:

    //按新的容量扩容Hash表  
        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];  
            transfer(newTable, initHashSeedAsNeeded(newCapacity));//将老的表中的数据拷贝到新的结构中  
            table = newTable;//修改HashMap的底层数组  
            threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);//修改阀值  
        }  
    

    此步骤也可能引起线程不安全

    当调用addEntry增加新的键值对后,如果键值对的总数量超过阈值,会调用resize扩容操作,这个操作会新生成一个新的容量的数组,然后对原数组的所有键值对重新进行计算和写入新数组,最后指向新的数组。
    当多个线程同时检测到键值对的数量超过阈值时,会同时调用resize操作,然后生成新数组,最后赋给map底层的数组table,然而只有最后一个线程生成的新数组赋给table,其他的线程全部丢失,而且,当某些线程赋值新数组给table时,其他数组才开始执行,就会用到新赋值给table的数组作为原始数组,这样,不同的线程可能会有不同的原始数组,造成HashMap混乱,出现线程不安全。

    6.resize中的transfer源码:

    //将老的表中的数据拷贝到新的结构中  
        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) {//如果是重新Hash,则需要重新计算hash值  
                        e.hash = null == e.key ? 0 : hash(e.key);  
                    }  
                    int i = indexFor(e.hash, newCapacity);//定位Hash桶  
                    e.next = newTable[i];//元素连接到桶中,这里相当于单链表的插入,总是插入在最前面
                    newTable[i] = e;//newTable[i]的值总是最新插入的值
                    e = next;//继续下一个元素  
                }  
            }  
        }  
    

    此步骤仍然可能出现线程不安全

    transfer()方法是数组转换操作,通过for (Entry<K,V> e : table)循环遍历旧table的每一个元素。而最最重要的操作出现了:
    e.next = newTable[i];
    newTable[i] = e;
    e = next;

    可以分割成四大操作:
    1.next=e.next//记录当前元素的下一个元素,在当前元素完成插入后,继续下一个元素的插入操作
    2.e.next = newTable[i]//将当前元素的下一个元素指向链表头
    3.newTable[i] = e//newTable[i]的值总是最新插入的值,即将当前元素替换成新的链表头
    4.e = next//处理完当前元素后,继续处理下一个元素

    首先举个例子
    假设有一个数组大小为2,在table[1]处有两个相同hashcode的元素,存放在对应的链表内,分别是key是3和key是7的元素。
    在这里插入图片描述
    通过resize后,要将旧数组中的两个元素转换到新数组中按照步骤为:
    第一次:
    1.next=e.next//即next=key(3).next=key(7)
    2.e.next = newTable[i]//即key(3).next=newTable[3]
    注意:int i = indexFor(e.hash, newCapacity);//定位Hash桶
    此时定位i为3,所以新数组大小为4
    3.newTable[i] = e//即newTable[3] = key(3)
    4.e = next//即e=key(7)
    新数组为:
    在这里插入图片描述
    第二次
    1.next=e.next//即next=key(7).next=null
    2.e.next = newTable[i]//即key(7).next=newTable[3]=key(3),上面第一步已经将newTable[3] = key(3)
    3.newTable[i] = e//即newTable[3] = key(7)
    4.e = next//即e=null结束旧数组转换新数组
    总结步骤为:
    在这里插入图片描述
    观察发现:每次将旧数组链表元素插入到新数组链表中时,总是将新的要插入的元素,插到链表头,这叫做链表的头插法。多线程下这有可能引起链表死循环,造成线程不安全。

    例子:
    假设有两个线程A和B,A线程执行完第一步next=e.next后阻塞,而B线程执行完,此时A才被唤醒,那么,转换图示及操作如下:

    在这里插入图片描述
    当e=key(3)时
    1.线程A阻塞时已经执行的过程为next=e.next=key(3).next=key(7)
    2.A被唤醒后执行e.next = newTable[i]//即key(3).next=null图中为线程B执行完,由于是链表的头插法,完成后的链表key(7)在key(3)前面,所以key(3)后为null
    3.newTable[i] = e//即newTable[3]=key(3)
    4.e=next//即e=key(7)

    当e=key(7)时
    1.next=key(7).next=key(3)
    2.e.next = newTable[i]即key(7).next=newTable[3]=key(3)
    3.newTable[i] = e//即newTable[3]=key(7)
    4.e=next//即e=key(3)

    当e=key(3)时
    1.next=key(3).next=null
    2.e.next = newTable[i]即key(3).next=newTable[3]=key(7)
    3.newTable[i] = e//即newTable[3]=key(3)
    4.e=next//即e=null,循环结束,但形成环路造成线程不安全。
    在这里插入图片描述
    所以以上部分就是讲解为什么HashMap为线程不安全,如果想解决此问题,要么使用ConcurrentHashMap要么给HashMap增加synchronized关键字保证线程安全。

    二.JDK1.8分析HashMap线程不安全

    在JDK1.8中HashMap实现了链表的尾插法,进行了put元素的优化
    那么,其他的一些地方的线程不安全类似于JDK1.7,这里就不再详解。
    如:put中的addEntry、addEntry中的resize
    而且JDK1.8的优化还在于使用红黑树,数据结构变为:数组+链表+红黑树,具体的优化后面会进一步解释。

    展开全文
  • HashMap线程不安全demo: 故障现象: 会报java.util.ConcurrentModificationException异常 解决方案: Map<String,String> map=new ConcurrentHashMap<>();

    HashMap线程不安全demo:
    在这里插入图片描述故障现象:
    会报java.util.ConcurrentModificationException异常
    解决方案:
    Map<String,String> map=new ConcurrentHashMap<>();

    展开全文
  • HashMap线程不安全原因及解决

    千次阅读 2020-03-27 15:09:58
    HashMap线程不安全的原因: 1、put的时候导致的多线程数据一致。 这个问题比较好想象,比如有两个线程A和B,首先A希望插入一个key-value对到HashMap中,首先计算记录所要落到的桶的索引坐标,然后获取到该桶里面的...

    HashMap线程不安全的原因:

    1、put的时候导致的多线程数据不一致。
    这个问题比较好想象,比如有两个线程A和B,首先A希望插入一个key-value对到HashMap中,首先计算记录所要落到的桶的索引坐标,然后获取到该桶里面的链表头结点,此时线程A的时间片用完了,而此时线程B被调度得以执行,和线程A一样执行,只不过线程B成功将记录插到了桶里面,假设线程A插入的记录计算出来的桶索引和线程B要插入的记录计算出来的桶索引是一样的,那么当线程B成功插入之后,线程A再次被调度运行时,它依然持有过期的链表头但是它对此一无所知,以至于它认为它应该这样做,如此一来就覆盖了线程B插入的记录,这样线程B插入的记录就凭空消失了,造成了数据不一致的行为。

    2、在扩容的时候,jdk1.8之前是采用头插法,当两个线程同时检测到hashmap需要扩容,在进行同时扩容的时候有可能会造成链表的循环,主要原因就是,采用头插法,新链表与旧链表的顺序是反的,在1.8后采用尾插法就不会出现这种问题,同时1.8的链表长度如果大于8就会转变成红黑树。

    我们俩就举个例子:
    如下:A的下一个指针是指向B的,即A->B->C
    在这里插入图片描述
    在进行扩容的时候,采用单链表的头插法,同一位置上新元素总会被放在链表的头部位置,那么原来的Entry元素就会往后一个位置。

    很有可能就会出现下面的情况:
    B的下一个指针指向了A
    在这里插入图片描述
    一旦几个线程都调整完成,就可能出现环形链表,
    在这里插入图片描述

    那么如何解决这个问题呢?

    1、使用HashTable替代HashMap

    当一个线程访问HashTable的同步方法时,其他线程如果也要访问同步方法,会被阻塞住。举个例子,当一个线程使用put方法时,另一个线程不但不可以使用put方法,连get方法都不可以。效率很低,所以基本不用。

    HashTable内方法上使用了synchronized。

    //Hashtable
    Map<String, String> hashtable = new Hashtable<>();
    

    2、类ConcurrentHashMap定义Map

    package java.util.concurrent;
    
    public class ConcurrentHashMap<K,V> extends AbstractMap<K,V>
    
        implements ConcurrentMap<K,V>, Serializable {
    
    ...
    
    }
    
    

    ConcurrentHashMap是JUC包中的一个类,方法内部使用了synchronized保证线程安全。

    //ConcurrentHashMap
    Map<String, String> concurrentHashMap = new ConcurrentHashMap<>();
    

    3、Collections类的synchronizedMap(Map m)方法可以返回一个线程安全的Map

    Map map = Collections.synchronizedMap(new HashMap<>());
    

    经别人测试,ConcurrentHashMap的性能很高!!!

    参考文章: https://blog.csdn.net/mrleeapple/article/details/91648659

    展开全文
  • HashMap线程不安全的场景

    千次阅读 2019-09-06 20:06:57
    HashMap线程不安全的场景 下图为HashMap的部分注解,大体意思是:如果多个线程同时访问HashMap,并且至少有一个线程做了结构上的修改,那么它必须在外部进行同步。 (结构修改是添加或删除一个或多个映射的任何操作;...

    HashMap线程不安全的场景

    下图为HashMap的部分注解,大体意思是:如果多个线程同时访问HashMap,并且至少有一个线程做了结构上的修改,那么它必须在外部进行同步。 (结构修改是添加或删除一个或多个映射的任何操作;仅仅修改键值不是结构修改。)

    HashMap的部分注解
    图片失效请点击

    put

    多个线程同时向Node[]的同一个位置插入时,会发生覆盖,只有一个线程的操作会被保留。

    如下图源码,假如有A、B两个线程,同时在执行put且数组下标都为1,两个线程同时运行到第一个红框位置,判断tab[1]为null,A线程先执行tab[1]=NodeA,B线程再执行tab[1]=NodeB,这样B线程的操作就会覆盖A线程的。

    第二个红框位置也会导致类似的问题,只不过是发生在链表上,而不是Node[]上

    put的部分源码
    图片失效请点击此处

    resize + get

    如果多个线程同时触发扩容resize,可能会形成环形链表,然后在调用get()获取一个不存在的元素时会发生死循环,导致CPU100%,JDK8已经解决了此问题。

    主要原因就是,JDK7在Hash冲突时采用头插法,扩容时若旧链表的元素还会Hash到同一个新链表,那么新链表与旧链表的顺序是反的(从 A-> B变成了B -> A),在1.8后采用尾插法就不会出现这种问题,同时1.8的链表长度如果大于8就会转变成红黑树。

    环形链表的形成
    Java HashMap的死循环
    JDK8的优化
    HashMap全面分析及JDK8对HashMap的相关优化

    remove

    感觉Remove想法有错误,但是找不到问题在哪,欢迎指正不要误导更多的人。手动笑哭

    多个线程同时remove会导致本来删除的元素重新回到链表上

    假如有链表 A -> B -> C -> D,线程1删除B,线程2删除C,同时执行到红框出,导致以下情况,B.next = C且 B.next = D,发生了数据不一致,若线程2先执行线程1再执行的话,线程2:A -> B -> D ,线程1:A -> C,结果为A -> C不是 A -> D。
    线程1:A.next = B.next = C
    线程2:B.next = C.next = D

    假如有链表 A -> B ,线程1删除B,线程2删除A,两个线程同时执行到红框处,然后如下执行
    线程2:tab[i] = A.next = B
    线程1:A.next = B.next = null
    结果为 tab[i] = B;也就是说本来应该被删除的B还存在

    remove的部分源码
    图片失效请点击此处

    size

    由于++size--size不是原子性操作,所以也会导致数据不一致

    参考文章

    展开全文
  • 我们都知道HashMap线程不安全的,但是HashMap的使用频率在所有map中确实属于比较高的。因为它可以满足我们大多数的场景了。 Map类继承图 上面展示了java中Map的继承图,Map是一个接口,我们常用的实现类有HashMap...
  • 三种解决方案 推荐第三种解法,第二种效率太低一般情况下直接抛弃 1.Hashtable替换HashMap Hashtable 是同步的,但由迭代器返回的 Iterator 和由所有 Hashtable 的“collection 视图方法”返回的 Collection ...
  • 阅读本文章之前需要了解HashMap基本原理,推荐阅读博主关于HashMap源码的逐行... 使用头插法的原因二、JDK1.8中HashMap线程不安全的原因JDK1.8解决扩容成环问题的方式 一、JDK1.7中HashMap线程不安全的原因 在JDK1.7
  • 我们都知道HashMap线程不安全的,但是HashMap的使用频率在所有map中确实属于比较高的。因为它可以满足我们大多数的场景了。 Map类继承图 上面展示了java中Map的继承图,Map是一个接口,我们常用的实现类有...
  • 一、new ArrayList 下面是jdk1.8的源码片段 /** * Constructs an empty list with an ...五、HashMap线程不安全解决方案 1.Collections.synchronizedMap(new HashMap()); 2. new ConcurrentHashMap();
  • 总说HashMap线程不安全的,不安全的,不安全的,那么到底为什么它是线程不安全的呢?要回答这个问题就要先来简单了解一下HashMap源码中的使用的存储结构(这里引用的是Java 8的源码,与7是一样的)和它的扩容机制...
  • 转载自 谈谈HashMap线程不安全的体现HashMap的原理以及如何实现,之前在JDK7与JDK8中HashMap的实现中已经说明了。那么,为什么说HashMap是线程不安全的呢?它在多线程环境下,会发生什么情况呢?1. resize死循环...
  • 如何应对HashMap线程不安全的问题?

    千次阅读 2019-06-12 23:02:35
    举个例子,当一个线程使用put方法时,另一个线程不但可以使用put方法,连get方法都可以。效率很低,所以都不会用。 Hashtable内方法上使用了synchronized。 2、类ConcurrentHashMap定义Map 源码是: ...
  • HashMap线程安全问题解决: 1>HashTable代替HashMap; 2>使用Collections工具类的synchronizedMap(Map<K,V> m) 返回由指定映射支持的同步(线程安全)映射。----这个方法是参数给一个线程安全的...
  • hashMap线程不安全的原因及表现

    千次阅读 2018-08-15 20:03:36
    其实只要有锁的机制,可以通过锁实现线程安全,我们在读写HashMap对象的时候加锁,以保障这个对象的线程安全,但代表HashMap本身是线程安全的,因为是外力(你自己加的锁)使然。 为啥HashMap内部加锁让它...
  • HashMap 线程安全问题

    万次阅读 多人点赞 2019-03-21 21:55:33
    我们紧接着上节ArrayList 线程安全问题讲下HashMap线程安全问题. 之前看书,书中经常会提及.HashTable是线程安全的,HashMap是线程非安全的.在多线程的情况下, HashMap会出现死循环的情况.此外,还会推荐使用新的JUC...
  • HashMap线程不安全导致的死循环和数据丢失 1.HashMap在扩容时由于线程安全问题产生死循环和数据丢失问题。 1.7之前采用头插法 1.8之后采用尾插法 采用尾插法只能解决循环问题,但是解决数据丢失问题,由于多...
  • 通过Entry内部的next变量可以知道使用的是链表,如果多个线程,在某一时刻同时操作HashMap并执行put操作,而有大于两个key的hash值相同,这个时候需要解决碰撞冲突,而采用拉链法解决碰撞冲突,这个时候两个线程如果...
  • HashMap线程不安全问题

    千次阅读 2015-09-13 11:35:03
    查看日志发现“java.util.ConcurrentModificationException”异常: 二、问题分析 创建虚拟机失败后,会下发命令删除虚拟机做回滚,而删除前会查询虚拟机,由于查询虚拟机很慢,起了多线程分别查计算、存储、网络...
  • HashMap线程安全

    2020-07-02 15:53:34
    HashMap线程安全的吗,为什么不是线程安全的。 不是线程安全的,因为多线程环境下,使用 HashMap 进行 put 操作可能会引起死循环,导致 CPU 利用率接近 100%,所以在并发情况下 HashMap 不是线程安全的。 如果有...
  • 首先HashMap线程不安全的。JDK1.7的时候采用头插法,多线程同时插入的时候,A线程在插入节点B,B线程也在插入,遇到容量不够开始扩容,重新hash,放置元素,采用头插法,后遍历到的B节点放入了头部,这样形成了环...
  • HashMap线程不安全resize()死循环过程分析原因分析 resize()死循环 在JDK1.8之前,HashMap在动态扩容时复制旧table中的链表结点到新扩容后的newTab中使用的是头插法,每个节点都是插入在链表的头部,这也是导致...
  • HashMap线程安全问题及解决方案

    千次阅读 2018-03-25 22:58:29
    为什么线程不安全个人觉得 HashMap 在并发时可能出现的问题主要是两方面,首先如果多个线程同时使用put方法添加元素,而且假设正好存在两个 put 的 key 发生了碰撞(根据 hash 值计算的 bucket 一样),那么根据 ...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 86,524
精华内容 34,609
关键字:

解决hashmap线程不安全