2024-03-14  阅读(3)
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。 本文链接:https://www.skjava.com/mianshi/baodian/detail/1897293049

回答

HashMap 是线程不安全的,主要体现在如下几个方面(Java 8)。

数据覆盖

在调用 put() 方法时,如果两个线程同时调用 put() 方法,且两个线程在同一个桶位置插入数据,这个一个线程的数据可能会被另一个线程给覆盖掉。

  • 线程 A 执行完代码 ① ,然后挂起。
  • 线程 B 执行完 ①、② 后,继续执行下面代码。
  • 线程 A 继续执行 ②,这个时候线程 A 的数据会覆盖掉线程 B 的数据

数据丢失

数据丢失可能会体现在多个地方,这里大明哥就介绍两个地方。

一、扩容过程中的元素迁移导致的数据丢失

在迁移元素到新的数组中时,原始数组中的链表会重新打散,拆分为两个链表,低位链表保留在原始索引位置,高位链表要迁移到新的索引位置。这个过程要遍历旧链表中的每一个节点进行重分配。如果此时有其他线程尝试插入新元素,而resize()操作尚未完成,可能会导致在resize()过程中新插入的数据被遗漏。

put() 部分源码

resize() 部分源码

比如线程 A 在执行 resize() 方法,线程 B 执行 putVal() 方法。

  • 线程 A 在处理最后一个节点,此时 next = e.next() == null,即代码 ① 处,线程 A 挂起。
  • 线程 B 执行执行到 ① 处的时候发现 (e = p.next) = null,所以将新节点插入其中。
  • 但是,对于线程 A 而言,next 已经为 null,它已经不会再处理这个链表了,所以会导致线程 B 新插入的结点互遗漏掉。

二、链表重组的线程安全问题

resize()方法中,为了保持元素的顺序,会对旧链表中的元素进行重新链接,形成两个链表,然后分别链接到新的桶数组中。如果在这个过程中,其他线程对这些链表进行修改(如插入、删除操作),可能会导致链表结构被破坏,从而导致元素丢失。

最后

其实 HashMap 中存在很多线程安全的问题,还有类似下面这段代码:

这两个参数定义如下:

transient int modCount;
transient int size;

modCountsize 连最基本的可见性都无法保证,何来的线程安全?

有一点要注意,Java 8 中并不会出现 Java 7 中那样的死循环链。但网上很多文章都说 Java 8 在多线程环境下不会有数据丢失问题,这是错误的,大明哥用一段代码来验证下:

public class HashMapTest {
    public static void main(String[] args) throws InterruptedException {
        Map<Integer,Integer> hashMap = new HashMap<>();
        ExecutorService service = Executors.newFixedThreadPool(100);

        for (int i = 0; i < 100; i++) {
            int threadNum = i;
            service.execute(() -> {
                for (int j = 0; j < 1000; j++) {
                    hashMap.put(threadNum * 1000 + j, j);
                }
            });
        }

        service.shutdown();
        service.awaitTermination(1, TimeUnit.HOURS);

        System.out.println("hashMap.size() = " + hashMap.size());
    }
}

启动 100 个线程,每个线程向 HashMap 插入 1000 个数据,如果数据不会丢失,那么最终的结果就是 100*1000,那事实呢?

HashMap 为了保证高性能,原本就不是为了线程安全而设计的,如果要使用线程安全的 HashMap ,我们可以使用如下两个:

  • Collections.synchronizedMap(Map):通过该方法可以创建一个所有方法都是同步的 Map,由于每次访问都需要进行同步,所以它在并发环境下性能较低。
  • ConcurrentHashMap:一个高性能的线程安全的 HashMap,推荐使用。

Java 面试宝典是大明哥全力打造的 Java 精品面试题,它是一份靠谱、强大、详细、经典的 Java 后端面试宝典。它不仅仅只是一道道面试题,而是一套完整的 Java 知识体系,一套你 Java 知识点的扫盲贴。

它的内容包括:

  • 大厂真题:Java 面试宝典里面的题目都是最近几年的高频的大厂面试真题。
  • 原创内容:Java 面试宝典内容全部都是大明哥原创,内容全面且通俗易懂,回答部分可以直接作为面试回答内容。
  • 持续更新:一次购买,永久有效。大明哥会持续更新 3+ 年,累计更新 1000+,宝典会不断迭代更新,保证最新、最全面。
  • 覆盖全面:本宝典累计更新 1000+,从 Java 入门到 Java 架构的高频面试题,实现 360° 全覆盖。
  • 不止面试:内容包含面试题解析、内容详解、知识扩展,它不仅仅只是一份面试题,更是一套完整的 Java 知识体系。
  • 宝典详情:https://www.yuque.com/chenssy/sike-java/xvlo920axlp7sf4k
  • 宝典总览:https://www.yuque.com/chenssy/sike-java/yogsehzntzgp4ly1
  • 宝典进展:https://www.yuque.com/chenssy/sike-java/en9ned7loo47z5aw

目前 Java 面试宝典累计更新 400+ 道,总字数 42w+。大明哥还在持续更新中,下图是大明哥在 2024-12 月份的更新情况:

想了解详情的小伙伴,扫描下面二维码加大明哥微信【daming091】咨询

同时,大明哥也整理一套目前市面最常见的热点面试题。微信搜[大明哥聊 Java]或扫描下方二维码关注大明哥的原创公众号[大明哥聊 Java] ,回复【面试题】 即可免费领取。

阅读全文