开发者社区> 编程指南针> 正文
阿里云
为了无法计算的价值
打开APP
阿里云APP内打开

都说 HashMap 是线程不安全的,到底体现在哪儿?

简介: 前言:我们都知道HashMap是线程不安全的,在多线程环境中不建议使用,但是其线程不安全主要体现在什么地方呢,本文将对该问题进行解密。 1.jdk1.7中的HashMap 在jdk1.8中对HashMap做了很多优化,这里先分析在jdk1.7中的问题,相信大家都知道在jdk1.7多线程环境下HashMap容易出现死循环,这里我们先用代码来模拟出现死循环的情况:
+关注继续查看

 imageimage.gif编辑

前言:我们都知道HashMap是线程不安全的,在多线程环境中不建议使用,但是其线程不安全主要体现在什么地方呢,本文将对该问题进行解密。

1.jdk1.7中的HashMap

在jdk1.8中对HashMap做了很多优化,这里先分析在jdk1.7中的问题,相信大家都知道在jdk1.7多线程环境下HashMap容易出现死循环,这里我们先用代码来模拟出现死循环的情况:

public class HashMapTest {
    public static void main(String[] args) {
        HashMapThread thread0 = new HashMapThread();
        HashMapThread thread1 = new HashMapThread();
        HashMapThread thread2 = new HashMapThread();
        HashMapThread thread3 = new HashMapThread();
        HashMapThread thread4 = new HashMapThread();
        thread0.start();
        thread1.start();
        thread2.start();
        thread3.start();
        thread4.start();
    }
}
class HashMapThread extends Thread {
    private static AtomicInteger ai = new AtomicInteger();
    private static Map<Integer, Integer> map = new HashMap<>();
    @Override
    public void run() {
        while (ai.get() < 1000000) {
            map.put(ai.get(), ai.get());
            ai.incrementAndGet();
        }
    }
}

image.gif

上述代码比较简单,就是开多个线程不断进行put操作,并且HashMap与AtomicInteger都是全局共享的。在多运行几次该代码后,出现如下死循环情形:

imageimage.gif编辑

其中有几次还会出现数组越界的情况:这里我们着重分析为什么会出现死循环的情况,通过jps和jstack命名查看死循环情况,结果如下:从堆栈信息中可以看到出现死循环的位置,通过该信息可明确知道死循环发生在HashMap的扩容函数中,根源在transfer函数中,jdk1.7中HashMap的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) {
                    e.hash = null == e.key ? 0 : hash(e.key);
                }
                int i = indexFor(e.hash, newCapacity);
                e.next = newTable[i];
                newTable[i] = e;
                e = next;
            }
        }
    }

image.gif

总结下该函数的主要作用:

在对table进行扩容到newTable后,需要将原来数据转移到newTable中,注意10-12行代码,这里可以看出在转移元素的过程中,使用的是头插法,也就是链表的顺序会翻转,这里也是形成死循环的关键点。下面进行详细分析。

1.1 扩容造成死循环分析过程

前提条件:

这里假设

    1. hash算法为简单的用key mod链表的大小。
    2. 最开始hash表size=2,key=3,7,5,则都在table[1]中。
    3. 然后进行resize,使size变成4。

    未resize前的数据结构如下:

    imageimage.gif编辑

    如果在单线程环境下,最后的结果如下:这里的转移过程,不再进行详述,只要理解transfer函数在做什么,其转移过程以及如何对链表进行反转应该不难。

    然后在多线程环境下,假设有两个线程A和B都在进行put操作。线程A在执行到transfer函数中第11行代码处挂起,因为该函数在这里分析的地位非常重要,因此再次贴出来。

    imageimage.gif编辑

    此时线程A中运行结果如下:线程A挂起后,此时线程B正常执行,并完成resize操作,结果如下:这里需要特别注意的点:由于线程B已经执行完毕,根据Java内存模型,现在newTable和table中的Entry都是主存中最新值:7.next=3,3.next=null。

    此时切换到线程A上,在线程A挂起时内存中值如下:e=3,next=7,newTable[3]=null,代码执行过程如下:

    newTable[3]=e ----> newTable[3]=3
    e=next ----> e=7

    image.gif

    此时结果如下:

    imageimage.gif编辑

    继续循环:

    e=7
    next=e.next ----> next=3【从主存中取值】
    e.next=newTable[3] ----> e.next=3【从主存中取值】
    newTable[3]=e ----> newTable[3]=7
    e=next ----> e=3

    image.gif

    结果如下:

    imageimage.gif编辑

    再次进行循环:

    e=3
    next=e.next ----> next=null
    e.next=newTable[3] ----> e.next=7 即:3.next=7
    newTable[3]=e ----> newTable[3]=3
    e=next ----> e=null

    image.gif

    注意此次循环:e.next=7,而在上次循环中7.next=3,出现环形链表,并且此时e=null循环结束。

    结果如下:

    imageimage.gif编辑

    在后续操作中只要涉及轮询hashmap的数据结构,就会在这里发生死循环,造成悲剧。

    1.2 扩容造成数据丢失分析过程

    遵照上述分析过程,初始时:imageimage.gif编辑线程A和线程B进行put操作,同样线程A挂起:

    imageimage.gif编辑

    此时线程A的运行结果如下:此时线程B已获得CPU时间片,并完成resize操作:同样注意由于线程B执行完成,newTable和table都为最新值:5.next=null。

    此时切换到线程A,在线程A挂起时:e=7,next=5,newTable[3]=null。

    执行newtable[i]=e,就将7放在了table[3]的位置,此时next=5。接着进行下一次循环:

    e=5
    next=e.next ----> next=null,从主存中取值
    e.next=newTable[1] ----> e.next=5,从主存中取值
    newTable[1]=e ----> newTable[1]=5
    e=next ----> e=null

    image.gif

    将5放置在table[1]位置,此时e=null循环结束,3元素丢失,并形成环形链表。并在后续操作hashmap时造成死循环。

    imageimage.gif编辑

    2.jdk1.8中HashMap

    在jdk1.8中对HashMap进行了优化,在发生hash碰撞,不再采用头插法方式,而是直接插入链表尾部,因此不会出现环形链表的情况,但是在多线程的情况下仍然不安全,这里我们看jdk1.8中HashMap的put操作源码:

    这是jdk1.8中HashMap中put操作的主函数, 注意第6行代码,如果没有hash碰撞则会直接插入元素。如果线程A和线程B同时进行put操作,刚好这两条不同的数据hash值一样,并且该位置数据为null,所以这线程A、B都会进入第6行代码中。

    假设一种情况,线程A进入后还未进行数据插入时挂起,而线程B正常执行,从而正常插入数据,然后线程A获取CPU时间片,此时线程A不用再进行hash判断了,问题出现:线程A会把线程B插入的数据给覆盖,发生线程不安全。

    这里只是简要分析下jdk1.8中HashMap出现的线程不安全问题的体现,后续将会对java的集合框架进行总结,到时再进行具体分析。

    总结

    首先HashMap是线程不安全的,其主要体现:

      1. 在jdk1.7中,在多线程环境下,扩容时会造成环形链或数据丢失。
      2. 在jdk1.8中,在多线程环境下,会发生数据覆盖的情况。

      版权声明:本文内容由阿里云实名注册用户自发贡献,版权归原作者所有,阿里云开发者社区不拥有其著作权,亦不承担相应法律责任。具体规则请查看《阿里云开发者社区用户服务协议》和《阿里云开发者社区知识产权保护指引》。如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。

      相关文章
      线程安全的集合
      日常coding,我们是不是经常用到ArrayList、HashSet、HashMap这样的集合?那你知不知道这些集合在多线程中是不安全的?
      22 0
      字节一面:go的协程比线程轻量,体现在哪?
      这里特意指出网络IO操作不会走上图的模型,否则要分配的系统线程M依然很多,程序很快就爆满了。这时G会和逻辑处理器P分离,并移动到netpoller,一旦网络轮询器通知网络读/写就绪,对应G就会重新分配到逻辑器处理器上来完成操作, 在此期间原系统线程M可以去做别的G。
      27 0
      为什么 HashMap 是线程不安全的?(1)
      为什么 HashMap 是线程不安全的?
      72 0
      线程安全性
      线程安全性 要编写线程安全的代码,其核心在于要对状态访问操作进行管理,特别是对共享的和可变的状态的访问。 "共享"意味着变量可以由多个线程同时访问,而可变则意味着变量的值在其生命周期内可以发生变化。
      840 0
      如何去学习网络安全
        对于一些不知道怎么去入手学习网络安全的,这里我给出一些学习资料的网址。可以根据这网站里面的视频学习或者通过做里面的挑战来提升自己。遇到不会的,自己可以去Google或百度上搜对应的writeup。然后自己在去实践,这样学习的更快,更有体会。
      830 0
      线程安全的观察者模式的设计
      观察者模式的应用,主要的行为就是注册和移除观察者(observer),以及通知所有已注册的Observers。
      1652 0
      设计模式之单例模式(线程安全)
      可以说单例模式是所有设计模式中最简单的一种。 单例模式就是说系统中对于某类的只能有一个对象,不可能出来第二个。 单例模式也是23中设计模式中在面试时少数几个会要求写代码的模式之一。主要考察的是多线程下面单例模式的线程安全性问题。
      794 0
      +关注
      编程指南针
      作者简介:阿里云特邀作者、Java领域优质创作者、CSDN博客专家 、掘金特邀作者、多年架构师设计经验、CSDN名师课堂讲师、腾讯课堂常驻讲师 主要内容:Java项目、毕业设计、简历模板、学习资料、面试题库、技术互助
      106
      文章
      0
      问答
      文章排行榜
      最热
      最新
      相关电子书
      更多
      低代码开发师(初级)实战教程
      立即下载
      阿里巴巴DevOps 最佳实践手册
      立即下载
      冬季实战营第三期:MySQL数据库进阶实战
      立即下载