写点什么

第一次听说 HashMap 也能并发安全,真心推荐这种玩法!

作者:采菊东篱下
  • 2025-02-19
    湖南
  • 本文字数:1606 字

    阅读完需:约 5 分钟

这篇文章,我们深入聊聊:读写锁如何保证 HashMap 成为一个线程安全的容器


1 编程范式例子

上图展示了使用读写锁对 HashMap 进行操作的编程范式,核心要点:

  • 单独的类用于封装对 HashMap 的读、写操作;

  • 读操作方法内部,先获取读锁,读取数据之后,释放读锁;

  • 写操作方法内部,先获取写锁,写入成功之后,释放写锁。

很多同学问我:”勇哥,假如读锁申请成功后,写锁会阻塞吗 ?“ 或者 ”写锁申请成功后,读锁会被阻塞吗?“ 。

答案是肯定的,读写必然互斥 。

笔者分别写两个简单的例子,并展示堆栈图,大家就可以一目了然。

3 读锁申请成功后,写锁会被阻塞

我们将 ReadWriteLockCache 的读操作修改如下:

然后编写 main 方法:

main 方法中,我们先后启动读线程、写线程 。

我们通过 IDEA 打印堆栈日志,发现:读线程先获取读锁,然后休眠 10 秒,这样读锁就不会释放,后面写线程尝试获取写锁时,写线程阻塞了。

3  写锁申请成功后,读锁会被阻塞

我们将 ReadWriteLockCache 的读操作代码还原,然后将写操作修改如下:

然后编写 Main 方法:

main 方法中,我们先后启动写线程、读线程 。

我们通过 IDEA 打印堆栈日志,发现:写线程先获取写锁,然后休眠 10 秒,这样写锁就不会释放,后面读线程尝试获取读锁时,线程阻塞了。

4 使用 ConcurrentHashMap 是不是更简单点

有的同学会问:使用 ConcurrentHashMap 是不是更简单点吗 ?

我们分两个层面来说明:

01 读写锁 + 多个 HashMap

读写锁可以操作多个 HashMap ,每次写操作需要同时变更多个 HashMap ,为了保证其一致性,故需要加锁,ConcurrentHashMap 并发容器在多线程环境下的线程安全也只是针对其自身,故从这个维度,选用读写锁是必然的选择 。

我们举 RocketMQ NameServer 的经典案例:

Broker 启动之后会向所有 NameServer 定期(每 30s)发送心跳包(路由信息),NameServer 会定期扫描 Broker 存活列表,如果超过 120s 没有心跳则移除此 Broker 相关信息,代表下线。

那么 NameServer 如何保存路由信息呢?

路由信息通过几个 HashMap 来保存,当 Broker 向 Nameserver 发送心跳包(路由信息),Nameserver 需要对 HashMap 进行数据更新,但我们都知道 HashMap 并不是线程安全的,高并发场景下,容易出现 CPU 100% 问题,所以更新 HashMap 时需要加锁,RocketMQ 使用了  JDK 的读写锁 ReentrantReadWriteLock 。

  1. 更新路由信息,操作写锁

  1. 查询主题信息,操作读锁

02  读写锁 + 1 个 HashMap

假如我们仅仅使用读写锁操作 1 个 HashMap ,那么我们需要分析下 ConcurrentHashMap 的原理。

1、 JDK 8 之前

从图中我们可以看出, ConcurrentHashMap 内部进行了 Segment 分段,Segment 继承了 ReentrantLock,可以理解为一把锁,各个 Segment 之间都是相互独立上锁的,互不影响。

同一个 Segment 的读写都需要加锁,即落在同一个 Segment 中的读、写操作是串行的,其读的并发性低于读写锁 + HashMap 的,

因此在 JDK 1.8 之前,ConcurrentHashMap 是落后于读写锁 + HashMap 的结构的

2、 JDK 1.8 及其后续版本

JDK 1.8 对 ConcurrentHashMap 代码进行了大幅优化,存储结构与 HashMap 非常类似,同时引入了 CAS 机制(轻量级) 来解决并发更新。

因此,相比读写锁操作 1 个 HashMap, 使用 ConcurrentHashMap 更具性能优势。

5 总结

这篇文章,我们深入剖析:读写锁如何保证 HashMap 成为一个线程安全的容器

1、读写锁编程范式

  • 单独的类用于封装对 HashMap 的读、写操作;

  • 读操作方法内部,先获取读锁,读取数据之后,释放读锁;

  • 写操作方法内部,先获取写锁,写入成功之后,释放写锁。

2、两个实验例子

  • 读锁申请成功后,写线程申请写锁会阻塞

  • 写锁申请成功后,读线程申请读锁会阻塞

我们用两个实验突出了读写锁的特性:读读不互斥,读写互斥,写写互斥 。

3、使用 ConcurrentHashMap 是不是更简单点

  • 假如需要操作 多个 HashMap ,那么读写锁更加有优势 ;

  • 假如仅仅操作 1 个 HashMap  ,  建议使用  JDK 1.8 ConcurrentHashMap ,性能会更好。

用户头像

还未添加个人签名 2023-02-14 加入

还未添加个人简介

评论

发布
暂无评论
第一次听说HashMap 也能并发安全,真心推荐这种玩法!_Java_采菊东篱下_InfoQ写作社区