【漫画】互斥锁ReentrantLock不好用?试试读写锁ReadWriteLock
ReentrantLock完美实现了互斥,完美解决了并发问题。但是却意外发现它对于读多写少的场景效率实在不行。此时ReentrantReadWriteLock来救场了!一种适用于读多写少场景的锁,可以大幅度提升并发效率,你必须会哦!
序幕

为何引入读写锁?
ReentrantReadWriteLock,顾名思义,是可重用的读写锁。
在读多写少的场合,读写锁对系统性能是很有好处的。因为如果系统在读写数据时均只使用独占锁,那么读操作和写操作间、读操作和读操作间、写操作和写操作间均不能做到真正的并发,并且需要相互等待。而读操作本身不会影响数据的完整性和一致性。
因此,理论上讲,在大部分情况下,应该可以允许多线程同时读,读写锁正是实现了这种功能。
划重点:读写锁适用于读多写少的情况。可以优化性能,提升易用性。
读写锁 ReadWriteLock
读写锁,并不是 Java 语言特有的,而是一个广为使用的通用技术,所有的读写锁都遵守以下三条基本原则:
- 允许多个线程同时读共享变量;
- 只允许一个线程写共享变量;
- 如果一个写线程正在执行写操作,此时禁止读线程读共享变量。
读写锁与互斥锁的一个重要区别就是读写锁允许多个线程同时读共享变量,而互斥锁是不允许的,这是读写锁在读多写少场景下性能优于互斥锁的关键。但读写锁的写操作是互斥的、独占的,当一个线程在写共享变量的时候,是不允许其他线程执行写操作和读操作。只要没有写操作,读取锁可以由多个读线程同时保持。读写锁访问约束如下表所示:
| 读写锁 | 读 | 写 |
|---|---|---|
| 读 | 非阻塞 | 阻塞 |
| 写 | 阻塞 | 阻塞 |
读写锁维护了一对相关的锁,一个用于只读操作,一个用于写入操作。
private final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
//读锁
private final Lock r = rwl.readLock();
//写锁
private final Lock w = rwl.writeLock();
为了对比读写锁和独占锁的区别,我们可以写一个测试代码,分别传入ReentrantLock 和 ReadLock,对比一下总耗时。
private static final ReentrantLock lock = new ReentrantLock();
private static final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
private static final Lock r = rwl.readLock();
public static String read(Lock lock, String key) throws InterruptedException {
r.lock();
try {
// 模拟读耗时多的场景 更能看出区别
Thread.sleep(1000 * 10);
return m.get(key);
} finally {
r.unlock();
}
}
快速实现一个缓存
回想一下工作中经常用到的缓存,例如缓存元数据,不就是一种典型的读多写少应用场景吗?缓存之所以能提升性能,一个重要的条件就是缓存的数据一定是读多写少的,例如元数据和基础数据基本上不会发生变化(写少),但是使用它们的地方却很多(读多)。
我们是不是可以用ReentrantReadWriteLock来手写一个缓存呢?先画一张图模拟简单的缓存流程吧:



String get(String key) throws InterruptedException {
String v = null;
r.lock();
log.info("{}获取读锁 time={}",Thread.currentThread().getName(),System.currentTimeMillis());
try {
v = m.get(key);
} finally {
r.unlock();
log.info("{}释放读锁 time={}",Thread.currentThread().getName(),System.currentTimeMillis());
}
if (v != null) {
log.info("{}缓存存在,返回结果 time={}",Thread.currentThread().getName(),System.currentTimeMillis());
return v;
}
w.lock();
log.info("{}缓存中不存在,查询数据库,获取写锁 time={}",Thread.currentThread().getName(),System.currentTimeMillis());
try {
log.info("{}二次验证 time={}",Thread.currentThread().getName(),System.currentTimeMillis());
v = m.get(key);
if (v == null) {
log.info("{}查询数据库完成 time={} ",Thread.currentThread().getName(),System.currentTimeMillis());
v = "value";
log.info("-------------验证写锁占有的时候 其他线程无法执行写操作和读操作----------------");
Thread.sleep(1000*5);
m.put(key, v);
}
} finally {
log.info("{}写锁释放 time={}",Thread.currentThread().getName(),System.currentTimeMillis());
w.unlock();
}
return v;
}

原创声明:本文来源于微信公众号【胖滚猪学编程】,持续更新JAVA\大数据干货,用漫画形式让编程so easy and interesting。转载请注明出处。
ReentrantReadWriteLock的特色功能
在J.U.C Lock包之ReentrantLock互斥锁,我们介绍了ReentrantLock相比synchronized的几大特色功能,例如公平锁、非阻塞获取锁、超时、中断。那么ReentrantReadWriteLock是否也有呢?
简单。。看看源码不就清楚了。以下源码都是在ReentrantReadWriteLock.java中撩出来的~ 剩下的我就不用多说了吧!如果不清楚这些方法可以回头看看J.U.C Lock包之ReentrantLock互斥锁
public ReentrantReadWriteLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
readerLock = new ReadLock(this);
writerLock = new WriteLock(this);
}
public boolean tryLock(long timeout, TimeUnit unit)
throws InterruptedException {
return sync.tryAcquireNanos(1, unit.toNanos(timeout));
}
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
读写锁的升级与降级
还想跟你聊聊锁的升级和降级。也许你是第一次听到,锁还有升级降级的功能。但其实不难理解,比如在读写锁中,写锁变为读锁是完全可行的方案,不会有任何问题,这里写锁变读锁就叫做锁的降级。
那么可以升级吗?熟话说降级容易,你只要天天不来上班就行了,升级可难哦。锁中也是,只是在锁中更加苛刻,完全不允许升级,即读锁无法升级为写锁。必须先释放读锁,才可以获取写锁。为什么不允许升级?试想有1000个读线程同时执行,同时升级为写锁,会发生什么?获取写锁的前提是读锁和写锁均未被占用,因此可能导致阻塞较长的时间,也可能发生死锁。
先写个代码验证一下吧,在(2)处我们实现了降级,程序是完全ok的,在(1)处如果你注释掉 r.unlock(),试图升级为读锁,你会发现程序会跑不下去的,据此可以验证我们所说的:读写锁可以降级、无法升级。
void processCachedData() {
// 获取读锁
r.lock();
if (!cacheValid) {
// 释放读锁 因为不允许读锁的升级 可以注释掉该行代码 整个程序会阻塞
r.unlock(); //(1)
// 获取写锁
w.lock();
try {
// 再次检查状态
if (!cacheValid) {
data = "胖滚猪学编程";
cacheValid = true;
}
// 释放写锁前 降级为读锁 降级是可以的
r.lock(); //(2)
} finally {
// 释放写锁
w.unlock();
}
}
// 此处仍然持有读锁
try {
System.out.println(data);
} finally {
r.unlock();
}
}
总结
读写锁适用于读多写少的情况。可以优化性能,提升易用性。缓存就是个很好的例子。
读写锁最大的特征是允许多个线程同时读共享变量。但是只允许一个线程写共享变量,且如果一个写线程正在执行写操作,此时禁止读线程读共享变量。
ReentrantReadWriteLock读写锁类似于 ReentrantLock,支持公平模式和非公平模式、支持非阻塞获取锁、超时、中断等特性。但是有一点需要注意,那就是只有写锁支持条件变量,读锁是不支持条件变量的,读锁调用 newCondition() 会抛出 UnsupportedOperationException 异常。
所以!我们必须了解各种锁的用途,才能在生产上选择最合适高效的方式。
原创声明:本文来源于微信公众号【胖滚猪学编程】,持续更新JAVA\大数据干货,用漫画形式让编程so easy and interesting。转载请注明出处。
本文转载自公众号【胖滚猪学编程】 用漫画让编程so easy and interesting!欢迎关注!形象来源于微信表情包【胖滚家族】喜欢可以下载哦~
【漫画】互斥锁ReentrantLock不好用?试试读写锁ReadWriteLock的更多相关文章
- go Mutex (互斥锁)和RWMutex(读写锁)
转载自: https://blog.csdn.net/skh2015java/article/details/60334437 golang中sync包实现了两种锁Mutex (互斥锁)和RWMute ...
- 【漫画】读写锁ReadWriteLock还是不够快?再试试StampedLock!
本文来源于公众号[胖滚猪学编程] 转载请注明出处! 在互斥锁ReentrantLock不好用?试试读写锁ReadWriteLock一文中,我们对比了互斥锁ReentrantLock和读写锁ReadWr ...
- 显式锁(三)读写锁ReadWriteLock
前言: 上一篇文章,已经很详细地介绍了 显式锁Lock 以及 其常用的实现方式- - ReetrantLock(重入锁),本文将介绍另一种显式锁 - - 读写锁ReadWriteLock. ...
- concurrent(三)互斥锁ReentrantLock & 源码分析
参考文档:Java多线程系列--“JUC锁”02之 互斥锁ReentrantLock:http://www.cnblogs.com/skywang12345/p/3496101.html Reentr ...
- C# 多线程编程之锁的使用【互斥锁(lock)和读写锁(ReadWriteLock)】
多线程编程之锁的使用[互斥锁(lock)和读写锁(ReadWriteLock)] http://blog.csdn.net/sqqyq/article/details/18651335 多线程程序写日 ...
- 读-写锁 ReadWriteLock & 线程八锁
读-写锁 ReadWriteLock: ①ReadWriteLock 维护了一对相关的锁,一个用于只读操作, 另一个用于写入操作. 只要没有 writer,读取锁可以由 多个 reader 线程同时保 ...
- Java 读写锁 ReadWriteLock 原理与应用场景详解
Java并发编程提供了读写锁,主要用于读多写少的场景,今天我就重点来讲解读写锁的底层实现原理@mikechen 什么是读写锁? 读写锁并不是JAVA所特有的读写锁(Readers-Writer Loc ...
- 线程中的读写锁ReadWriteLock
Lock锁还有两个非常强大的类 ReadWriteLock接口实现类ReentrantReadWriteLock(非常重要的锁) 想实现 读取的时候允许多线程并发访问,写入的时候不允许. 这种效果.. ...
- JUC回顾之-可重入的互斥锁ReentrantLock
1.什么是可重锁ReentrantLock? 就是支持重新进入的锁,表示该锁能够支持一个线程对资源的重复加锁. 2.ReentrantLock分为公平锁和非公平锁:区别是在于获取锁的机制上是否公平. ...
随机推荐
- 功能测试--聊天功能测试&微信聊天
微信聊天功能测试 发送对象 普通用户.公众号.群.其他特殊主体 衍生功能 转发.语音转文字.删除等 消息发送 单聊.群聊.语音.文字.图片.表情.链接.字符及长度 消息管理 发布通知.接受通知.发文件 ...
- SpringBoot与单元测试JUnit的结合
有些人认为,写单元测试就是在浪费时间 ,写完代码,依然还是能够进行测试的.但是,还是建议写单元测试的,可以让你的条理更加清晰,而且当某个功能出现问题时,可能通过单元测试很容易的定位和解决问题.本文主要 ...
- redis:安装及基础知识(一)
Redis官网:https://redis.io/ Redis中文网:http://www.redis.cn/ Redis 是一个开源的,内存中的数据结构存储系统,它可以用作数据库.缓存和消息中间件. ...
- 挑战全网最幽默的Vuex系列教程:第三讲 Vuex旗下的Mutation
写在前面 上一讲「Vuex 旗下的 State 和 Getter」,告诉了我们怎么去使用仓库 store 中的状态数据.当然,光会用肯定还不够,大部分的应用场景还得对这些状态进行操控,那么具体如何操控 ...
- python 基础篇 自定义函数
多态 我们可以看到,Python 不用考虑输入的数据类型,而是将其交给具体的代码去判断执行,同样的一个函数(比如这边的相加函数 my_sum()),可以同时应用在整型.列表.字符串等等的操作中. 在编 ...
- MySQL主从复制,主主复制,半同步复制
实验环境: 系统:CentOS Linux release 7.4.1708 (Core) mariadb:mariadb-server-5.5.56-2.el7.x86_64 node1:172.1 ...
- XSS语义分析的阶段性总结(二)
本文首发于“合天智汇”微信公众号,作者:Kale 前言 上次分享了javascript语义分析,并且简单介绍了新型xss扫描器的一些想法,如何在不进行大量fuzz的情况下又能准确的检测出xss漏洞,这 ...
- 关于mysql的范式——反范式的思路
数据库的设计,是有模式的,就是在实际生产的项目中,按照怎样怎样步骤的去做.减少冗余呀,一对多呀等等. 那么回归到一个问题:数据库究竟是为了添加,还是为了查询?这个问题有些轴,以 增删改查四律而言,都是 ...
- java中ThreadLocal的使用
文章目录 在Map中存储用户数据 在ThreadLocal中存储用户数据 java中ThreadLocal的使用 ThreadLocal主要用来为当前线程存储数据,这个数据只有当前线程可以访问. 在定 ...
- Yii项目Security加密解密类提取
<?php /** * @link http://www.yiiframework.com/ * @copyright Copyright (c) 2008 Yii Software LLC * ...