读写锁 + HashMap 超级组合,真心推荐!

开发 前端
很多同学问我:”勇哥,假如读锁申请成功后,写锁会阻塞吗 ?“ 或者 ”写锁申请成功后,读锁会被阻塞吗?“ 。答案是肯定的,读写必然互斥 。

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

图片图片

1.编程范式例子

图片图片

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

  • 单独的类用于封装对 HashMap 的读、写操作;
  • 读操作方法内部,先获取读锁,读取数据之后,释放读锁;
  • 写操作方法内部,先获取写锁,写入成功之后,释放写锁。

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

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

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

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

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

图片图片

然后编写 main 方法:

图片图片

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

图片图片

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

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

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

图片图片

然后编写 Main 方法:

图片图片

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

图片图片

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

4.使用 ConcurrentHashMap 是不是更简单点

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

我们分两个层面来说明:

1)读写锁 + 多个 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 。

  • 更新路由信息,操作写锁

图片图片

  • 查询主题信息,操作读锁

图片图片

2)读写锁 + 1 个 HashMap

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

  • JDK 8 之前

图片图片

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

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

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

  •  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 ,性能会更好。
责任编辑:武晓燕 来源: 勇哥Java实战
相关推荐

2024-01-29 01:08:01

悲观锁递归锁读写锁

2024-05-15 09:41:22

乐观锁编程

2019-11-28 16:00:06

重入锁读写锁乐观锁

2023-03-10 15:45:03

Golang公平锁

2013-06-06 13:10:44

HashMap无锁

2021-07-06 08:37:29

Redisson分布式

2024-06-11 00:01:00

并发validate场景

2018-01-01 05:23:13

服务化读写分离架构

2022-02-14 15:07:48

进程FileChanne线程

2024-10-10 09:40:29

2020-09-26 23:09:00

Linux系统编程读写锁

2023-01-04 13:43:24

读写锁AQS共享模式

2023-06-02 08:29:24

https://wwMutex

2020-09-16 07:56:28

多线程读写锁悲观锁

2021-12-16 18:38:13

面试Synchronize

2025-01-09 06:00:00

Checkmate监控系统开源

2023-01-06 09:40:20

项目性能

2022-05-07 07:35:44

工具读写锁Java

2024-08-12 17:36:54

2024-11-14 09:46:56

点赞
收藏

51CTO技术栈公众号