问小白 wenxiaobai
资讯
历史
科技
环境与自然
成长
游戏
财经
文学与艺术
美食
健康
家居
文化
情感
汽车
三农
军事
旅行
运动
教育
生活
星座命理

谈谈HashMap的线程安全问题与解决方案

创作时间:
作者:
@小白创作中心

谈谈HashMap的线程安全问题与解决方案

引用
CSDN
1.
https://blog.csdn.net/Wzp20020903/article/details/145838535

目录
一、为什么说HashMap是线程不安全的?
1.1 数据覆盖
1.2 size 字段不准确
1.3 脏数据
二、如何解决HashMap的线程安全问题?
2.1. 使用 ConcurrentHashMap(首选方案)
2.2 使用 Collections.synchronizedMap()
2.3 手动显式加锁
三、方案对比与选型建议
四、小小总结

一、为什么说HashMap是线程不安全的?

HashMap 是Java中最常用的数据结构之一,以其高效的查找和插入(平均O(1)时间复杂度)著称。然而,在多线程环境下,HashMap的线程不安全问题会导致数据不一致程序崩溃甚至死循环。HashMap线程不安全大致有以下的原因:

1.1 数据覆盖

当多个线程同时调用 put() 方法插入键值对时,若发生哈希冲突(两个键被分配到同一个哈希桶),可能导致数据覆盖。

场景示例: 线程A和线程B同时发现某个哈希桶为空(table[i] == null),并各自插入新节点。后插入的节点会覆盖前一个节点的数据

底层原因:插入操作非原子性(检查桶状态 → 创建节点 → 插入链表/红黑树)。

代码示例:

public class HashMapTest {
    public static void main(String[] args) throws InterruptedException {
        HashMap<Integer,Integer> hashMap = new HashMap<>();
        ExecutorService executorService= Executors.newFixedThreadPool(10);
        //启动10个线程,每个线程都往hashMap中添加1000个元素
        for (int i = 0; i < 10; i++) {
            final int threadId=i;
            executorService.execute(()->{
                for (int j = 0; j < 1000; j++) {
                    //这里保证了key是不会重复的,这样保证了覆盖不是因为key名字相同而导致的覆盖
                    int key=threadId*1000+j;
                    hashMap.put(key,key);
                }
            });
        }
        executorService.shutdown();
        while (!executorService.isTerminated()){
            Thread.sleep(100);
        }
        //预期
        System.out.println("预期size:"+10*1000);
        //实际
        System.out.println("实际的hashMap size:"+hashMap.size());
    }
}  

运行结果:
可以看到每一次的运行出现数据覆盖的情况还不一样;

1.2 size 字段不准确

HashMap 的size 字段记录元素数量,但多线程下size++ 和 size-- 是非原子操作

场景: 1 线程B读取size=10,2 线程A读取 size=10,3 线程B同插入数据并更新 size=11,4 线程A写入 size=11,覆盖了B的更新(这些是高并发环境下的执行顺序)。 结果:size 与实际元素数量不一致(实际应该是12)

1.3 脏数据

一个线程正在修改哈希表结构(如扩容、树化),另一线程尝试读取数据时,可能读到中间状态

示例: 线程A正在迁移哈希桶,线程B读取到未完全迁移的旧桶,导致获取到 null 或部分数据。

根本原因:缺乏内存可见性保证(未使用volatile或锁)。

二、如何解决HashMap的线程安全问题?

2.1. 使用 ConcurrentHashMap(首选方案)

ConcurrentHashMap是专为高并发设计的线程安全哈希表,性能接近HashMap,且无需手动同步。

实现原理: JDK 1.7:分段锁(Segment),每个段独立加锁,降低锁竞争。 JDK 1.8+:CAS + synchronized 锁单个哈希桶(Node),锁粒度更细。

代码例子:

public class HashMapTest {
    public static void main(String[] args) throws InterruptedException {
        //这里换成了线程安全的ConcurrentHashMap
        Map<Integer,Integer> hashMap = new ConcurrentHashMap<>();
        ExecutorService executorService= Executors.newFixedThreadPool(10);
        //启动10个线程,每个线程都往hashMap中添加1000个元素
        for (int i = 0; i < 10; i++) {
            final int threadId=i;
            executorService.execute(()->{
                for (int j = 0; j < 1000; j++) {
                    //这里保证了key是不会重复的,这样保证了覆盖不是因为key名字相同而导致的覆盖
                    int key=threadId*1000+j;
                    hashMap.put(key,key);
                }
            });
        }
        executorService.shutdown();
        while (!executorService.isTerminated()){
            Thread.sleep(100);
        }
        //预期
        System.out.println("预期size:"+10*1000);
        //实际
        System.out.println("实际的hashMap size:"+hashMap.size());
    }
}  

结果:
可以看到使用了ConcurrentHashMap类之后就不会出现值被覆盖的问题了,所以最后的数量跟预期的数量一致

优点:高并发性能优异; 支持高吞吐量的读写操作;

2.2 使用 Collections.synchronizedMap()

通过包装类对所有方法加锁实现线程安全,但性能较低。锁粒度粗锁住整个Map,高并发下性能差。

Map<Integer, String> safeMap = Collections.synchronizedMap(new HashMap<>());

对比第一种方法可以看出运行时长变长

(效率虽低但都能实现线程安全)

适用场景:低并发场景或旧代码快速迁移

2.3 手动显式加锁

通过synchronizedReentrantLock 控制并发灵活性高但复杂度高

适用场景: 需要跨多个操作的复杂同步逻辑(因为可以设置在哪里开始上锁,哪里释放锁,,如事务性操作)。

三、方案对比与选型建议

四、小小总结

问题根源HashMap的线程不安全源于非原子操作、缺乏可见性保证和结构修改的竞争条件(也就是不上锁)

解决方案:优先选择ConcurrentHashMap,它在高并发下性能优异且易于使用。

设计原则:根据业务场景权衡性能与复杂度,避免不必要的同步开销(见机行事好吧)。

最后祝大家都能成为技术大牛,加油哇!

© 2023 北京元石科技有限公司 ◎ 京公网安备 11010802042949号