Java并发编程(八):HashMap非线程安全问题解析与应对

大家好,我是欧阳方超,微信公众号同名。在这里插入图片描述

1 概述

HashMap是一种非常常用的数据结构,用于存储键值对。它利用哈希表的原理实现,通过特定的哈希函数计算键的哈希值,然后就能快速在内存中找到对应的存储位置,进而进行值的存储及获取。在并发编程系列文章中,我们已经看到过自定义类的非线程安全性,本篇就介绍下JDK中这个常用却又非线程安全的类——HashMap。

2 HashMap非线程安全问题

2.1 并发扩容导致死循环或数据丢失

HashMap在多线程环境下会暴漏出一些问题,因为它不是线程安全的。这意味着当多个线程同时对一个HashMap进行操作时,可能会导致数据不一致或其他错误情况。这是由于HashMap的各种操作,如put、get、remove等,都没有进行同步处理。当HashMap执行put操作时,尤其是当元素个数达到一定阈值,需要进行扩容的时候,内部会进行复杂的计算,包括重新计算哈希值、复制元素等操作。假设这样一个场景,有两个线程thread1和thread2同时对HashMap进行操作,如果thread1执行put操作时触发了扩容操作,而此时thread2也在执行put或get操作,那么就可能出现问题,thread2可能会读取到不一致的数据。
为了更直观地展示HashMap的非线程安全性,下面看一个示例:

public class HashMapTest {

    public static void main(String[] args) {
        Map<Integer, Object> unsafeMap = new HashMap<>(2, 0.75f);
        //创建CountDownLatch,用于等待两个线程都完成操作
        CountDownLatch countDownLatch = new CountDownLatch(2);
        Thread thread1 = new Thread(() -> {
            for (int i = 0; i < 10000; i++) {
                unsafeMap.put(i, i);
            }
            countDownLatch.countDown();
        });

        Thread thread2 = new Thread(() -> {
            for (int i = 10000; i < 20000; i++) {
                unsafeMap.put(i, i);
            }
            countDownLatch.countDown();
        });
        thread1.start();
        thread2.start();
        try {
            //等待两个线程都完成操作
            countDownLatch.await();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println("最终HashMap的大小为:" + unsafeMap.size());
    }
}

上面的示例中,创建了两个线程thread1、thread2,这两个线程都向同一个HashMap中放入10000个键值对。理论上HashMap的大小应该是20000,但由于HashMap不是线程安全的,在put操作过程中可能会出现数据覆盖、丢失等情况,程序运行结果为:

最终HashMap的大小为:19423

2.2 非线程安全的迭代器

当使用HashMap的迭代器变量集合时,如果遍历过程中有其他线程修改了HashMap的结构,此时会发生ConcurrentModificationException异常。因为创建迭代器时,会记录下modCount值,当HashMap结构发生变化时,该变量也会变化,从而导致抛出异常。
下面的示例展示了这个问题:

public class HashMapTest {

    public static void main(String[] args) {
        Map<Integer, Object> unsafeMap = new HashMap<>(2, 0.75f);
        //创建CountDownLatch,用于等待两个线程都完成操作
        CountDownLatch countDownLatch = new CountDownLatch(2);
        Thread thread1 = new Thread(() -> {
            for (int i = 0; i < 10000; i++) {
                unsafeMap.put(i, i);
            }
            countDownLatch.countDown();
        });

        Thread thread2 = new Thread(() -> {
            Iterator<Integer> iterator = unsafeMap.keySet().iterator();
            while (iterator.hasNext()) {
                Integer next = iterator.next();
            }
            countDownLatch.countDown();
        });
        thread1.start();
        thread2.start();
        try {
            //等待两个线程都完成操作
            countDownLatch.await();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println("最终HashMap的大小为:" + unsafeMap.size());
    }
}

上面的示例中,thread1线程往HashMap放入键值对,同时thread2线程使用迭代器遍历HashMap,可能回抛出异常,程序运行结果:

Exception in thread "Thread-1" java.util.ConcurrentModificationException
	at java.util.HashMap$HashIterator.nextNode(HashMap.java:1445)
	at java.util.HashMap$KeyIterator.next(HashMap.java:1469)

3 多线程环境下的替代方案

在多线程环境下,如果要使用类似HashMap的功能,该怎么办呢?Java为开发者提供了ConcurrentHashMap,还可以使用Collections工具类提供的synchronized系列的方法,如synchronizedMap(),可以将HashMap封装成线程安全的集合。
比如在上面两个线程往HashMap放入键值对的示例中,如果使用Collections.synchronizedMap()方法来包装HashMap,就能一定程度上保证同步操作,

Map<Integer, Object> unsafeMap = new HashMap<>(2, 0.75f);
        Map<Integer, Object> threadSafeMap= Collections.synchronizedMap(unsafeMap);

调整后,程序运行结果为:

最终HashMap的大小为:20000

需要注意的是,虽然Collections.synchronizedMap() 来包装 HashMap,可以提供一定的同步机制,但这并不保证复合操作(例如迭代和修改)是安全的。特别是,当一个线程在修改映射时,另一个线程在迭代它,就可能导致并发修改,从而引发异常。所以,多线程环境下推荐使用ConcurrentHashMap,它是为并发访问设计的。

5 总结

通过示例展示了HashMap的非线程安全性,为确保线程安全,介绍了ConcurrentHashMap和Collections工具类两种同步机制,更推荐使用 ConcurrentHashMap,因为它通常是处理 Java 中并发操作的最佳实践,因为它设计用于高并发且不会造成锁的开销。
我是欧阳方超,把事情做好了自然就有兴趣了,如果你喜欢我的文章,欢迎点赞、转发、评论加关注。我们下次见。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值