这篇文章,我们深入聊聊:读写锁如何保证 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 ,性能会更好。