欢迎来到《并发王者课》,本文是该系列文章中的第15篇

在上篇文章中,我们介绍了Java中锁的基础Lock接口。在本文中,我们将介绍Java中锁的另外一个重要的基本型接口,即ReadWriteLock接口。

在探索Java中的并发时,ReadWriteLock无疑是重要的,然而理解它却并不容易。如果你此前曾经检索资料,应该会发现大部分的文章对它的描述都比较晦涩难懂,或连篇累牍的源码陈列,或隔靴搔痒的三言两语,既说不到重点,也说不清来龙去脉。

所以,在本文中我们会将介绍的重点放在对思路的理解上,而不是对源码的解读上。对于源码以及其背后的知识,我们将在后面的更高级的系列中进行讲解。

一、理解ReadWriteLock存在的价值

理解ReadWriteLock,首页要理解它存在的意义是什么。换言之,它要解决什么问题。为此,我们不妨从下图着手一探究竟。

不知你看明白了没有,这幅图所表达的有三层含义:

  • 大量线程在竞争同一份资源;
  • 这些线程中有的是读请求,有的是写请求
  • 在多个线程的请求中,读请求明显高于写请求

这样的场景是否似曾相识?没错,它就是典型的缓存应用场景

众所周知,缓存的存在是为了提高应用的读写性能。一方面,我们需要通过缓存拦截大量的读数据的请求。另一方面,我们也需要不定期地更新缓存。但总体而言,更新缓存的次数远远小于读缓存的次数

在这个过程中,关键问题在于,为了保持数据一致性,我们在读写缓存的时候,不能让读请求拿到脏数据,这就需要用到锁。然而,更关键的问题在于,虽然读写之间需要互斥,但读与读之间不可以互斥

总结来说,这个问题主要有下面这几个要点:

  • 数据允许多个线程同时读取,但只允许一个线程进行写入
  • 在读取数据的时候,不可以存在写操作或者写请求
  • 在写数据的时候,不可以存在读请求

如果你对此仍然有些迷茫,那么下面这张图建议你收藏,这张图正是ReadWriteLock对问题的概述和它的解决方案,也是诠释ReadWriteLock最好的一幅图。

在你没有理解ReadWriteLock之前,你会觉得它十分晦涩且源码枯燥。然而,一旦你理解它要解决的问题,以及它所提供的方案后,你会发现它的设计竟然如此巧妙。它竟然设计了两种截然不同的锁,其中一把正如我们此前认知的那样是线程互斥的,而另一把锁竟然可以为多个线程所共享!两把锁的完美配合,解决了并发读写的场景问题。

在恍然大悟后,所谓源码不过是队列与共享,它们是ReadWriteLock的一种实现方式,而不是阻挡你理解的绊脚石。

二、自主实现ReadWriteLock

在理解了ReadWriteLock背后的问题和它的解决思路之后,我们就可以完全抛开JDK中的源码自己实现一把读写锁。

public class ReadWriteLock{

  private int readers       = 0;
private int writers = 0;
private int writeRequests = 0; public synchronized void lockRead() throws InterruptedException{
while(writers > 0 || writeRequests > 0){
wait();
}
readers++;
} public synchronized void unlockRead(){
readers--;
notifyAll();
} public synchronized void lockWrite() throws InterruptedException{
writeRequests++; while(readers > 0 || writers > 0){
wait();
}
writeRequests--;
writers++;
} public synchronized void unlockWrite() throws InterruptedException{
writers--;
notifyAll();
}
}

在读锁lockRead()中,是不允许有写请求写操作的。如果有,那么读请求将进入等待。

而在lockWrite()中,同时不允许读请求和其他写操作的存在,此时只允许有一个写请求

以上就是读写锁简单的自主实现方式。当然,它是不完善的,只是基本的示例。它没有考虑到基本的线程重入问题,真实情况也比它复杂很多,但你理解它的意思就好。

三、Java中的ReadWriteLock是如何实现的

最后,我们再来看JDK中的ReadWriteLock实现的一些基本思路。ReadWriteLock和我们上篇所说的Lock接口以及其他类的基本关系如下图所示:

可以看到,JDK中的读写锁的实现是在ReentrantReadWriteLock这个类中。ReentrantReadWriteLock包含了两个内部类:ReadLock和WriteLock,而这两个类又实现了Lock接口。

读写锁的升级与降级

读写锁的升级与降级是ReentrantReadWriteLock中的一个重要知识点,也是高频的面试题。

从读锁到写锁,称之为锁的升级,反之为锁的降级。理解读写锁的升级和降级,最直观的方式是写代码验证。

代码片段1,先获取读锁,再获取写锁。

public class ReadWriteLockDemo {
public static void main(String[] args) {
ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
readWriteLock.readLock().lock();
System.out.println("已经获取读锁...");
readWriteLock.writeLock().lock();
System.out.println("已经获取写锁...");
}
}

输出结果如下:

已经获取读锁...

代码片段2,先获取写锁,再获取读锁:

public class ReadWriteLockDemo {
public static void main(String[] args) {
ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
readWriteLock.writeLock().lock();
System.out.println("已经获取写锁...");
readWriteLock.readLock().lock();
System.out.println("已经获取读锁...");
}
}

输出结果如下:

已经获取写锁...
已经获取读锁... Process finished with exit code 0

这样一来,结果已经十分明了。ReentrantReadWriteLock支持锁的降级,但不支持锁的升级

读写锁中的公平性

在前面的文章中,我们讲过线程饥饿的由来和后果,所以良好的并发工具类在设计时都会考虑到公平性,ReentrantReadWriteLock也是如此。

在ReentrantReadWriteLock中,同时提供了公平和非公平两种模式,且默认为非公平模式。从下面摘取的源码片段中,可以清晰地看到。

 public ReentrantReadWriteLock() {
this(false);
} /**
/**
* Creates a new {@code ReentrantReadWriteLock} with
* default (nonfair) ordering properties.
*/
public ReentrantReadWriteLock() {
this(false);
} /**
* Creates a new {@code ReentrantReadWriteLock} with
* the given fairness policy.
*
* @param fair {@code true} if this lock should use a fair ordering policy
*/
public ReentrantReadWriteLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
readerLock = new ReadLock(this);
writerLock = new WriteLock(this);
}

小结

以上就是关于读写锁的全部内容。在本文中,我们从缓存问题出发,接着从ReadWriteLock中寻找答案,以便能从更轻松的角度理解ReadWriteLock的来龙去脉。

理解ReadWriteLock的关键不在于对源码的剖析,而在于对其思路的理解。

另外,我们简单地介绍了ReentrantReadWriteLock中的一些关键知识点,但诸如其背后的AQS等并没有展开陈述。对此也不必着急,我们会在后面有详细的分析介绍。

正文到此结束,恭喜你又上了一颗星

夫子的试炼

  • 尝试在示例代码中增加对读写线程的重入支持。

延伸阅读与参考资料

关于作者

关注公众号【庸人技术笑谈】,获取及时文章更新。记录平凡人的技术故事,分享有品质(尽量)的技术文章,偶尔也聊聊生活和理想。不贩卖焦虑,不做标题党。

如果本文对你有帮助,欢迎点赞关注监督,我们一起从青铜到王者

并发王者课-铂金2:豁然开朗-“晦涩难懂”的ReadWriteLock竟如此妙不可言的更多相关文章

  1. 并发王者课-铂金1:探本溯源-为何说Lock接口是Java中锁的基础

    欢迎来到<并发王者课>,本文是该系列文章中的第14篇. 在黄金系列中,我们介绍了并发中一些问题,比如死锁.活锁.线程饥饿等问题.在并发编程中,这些问题无疑都是需要解决的.所以,在铂金系列文 ...

  2. 并发王者课-铂金6:青出于蓝-Condition如何把等待与通知玩出新花样

    欢迎来到<[并发王者课](https://juejin.cn/post/6967277362455150628)>,本文是该系列文章中的**第19篇**. 在上一篇文章中,我们介绍了阻塞队 ...

  3. 并发王者课-铂金8:峡谷幽会-看CyclicBarrier如何跨越重峦叠嶂

    欢迎来到<并发王者课>,本文是该系列文章中的第21篇,铂金中的第8篇. 在上一篇文章中,我们介绍了CountDownLatch的用法.在协调多线程的开始和结束时,CountDownLatc ...

  4. 并发王者课-铂金9:互通有无-Exchanger如何完成线程间的数据交换

    欢迎来到<并发王者课>,本文是该系列文章中的第22篇,铂金中的第9篇. 在前面的文章中,我们已经介绍了ReentrantLock,CountDownLatch,CyclicBarrier, ...

  5. 并发王者课-铂金10:能工巧匠-ThreadLocal如何为线程打造私有数据空间

    欢迎来到<并发王者课>,本文是该系列文章中的第23篇,铂金中的第10篇. 说起ThreadLocal,相信你对它的名字一定不陌生.在并发编程中,它有着较高的出场率,并且也是面试中的高频面试 ...

  6. 并发王者课 - 青铜4:synchronized用法初体验

    在前面的文章<双刃剑-理解多线程带来的安全问题>中,我们提到了多线程情况下存在的线程安全问题.本文将以这个问题为背景,介绍如何通过使用synchronized关键字解这一问题.当然,在青铜 ...

  7. 并发王者课 - 青铜 2:峡谷笔记 - 简单认识Java中的线程

    在前面的<兵分三路:如何创建多线程>文章中,我们已经通过Thread和Runnable直观地了解如何在Java中创建一个线程,相信你已经有了一定的体感.在本篇文章中,我们将基于前面的示例代 ...

  8. 并发王者课-青铜5:一探究竟-如何从synchronized理解Java对象头中的锁

    在前面的文章<青铜4:synchronized用法初体验>中,我们已经提到锁的概念,并指出synchronized是锁机制的一种实现.可是,这么说未免太过抽象,你可能无法直观地理解锁究竟是 ...

  9. 并发王者课-青铜7:顺藤摸瓜-如何从synchronized中的锁认识Monitor

    在前面的文章中,我们已经体验过synchronized的用法,并对锁的概念和原理做了简单的介绍.然而,你可能已经察觉到,有一个概念似乎总是和synchronized.锁这两个概念如影相随,很多人也比较 ...

随机推荐

  1. 二、jmeter模拟请求头及监听器之结果树

    一.模拟请求头 利用jmeter发送http请求时,被接收的服务端会对发送的该请求进行初步判断,如果不是web端发送的请求就会被打回导致请求不通,这时候需要模拟请求头,模拟正常的用户行为进行发送请求 ...

  2. JVM虚拟机-垃圾回收机制与垃圾收集器概述

    目录 前言 什么是垃圾回收 垃圾回收的区域 垃圾回收机制 流程 怎么判断对象已经死亡 引用计数法 可达性分析算法 不可达的对象并非一定会回收 关于引用 强引用(StrongReference) 软引用 ...

  3. [敏杰开发]Beta阶段测试报告

    [敏杰开发]Beta阶段测试报告 这是一篇软工课程博客 项目 内容 课程:北航-2020-春-软件工程 博客园班级博客 作业要求 Beta阶段测试报告 我们在这个课程的目标是 测试项目 这个作业在哪个 ...

  4. mysql多线程备份与还原工具mydumper

    (一)mydumper介绍 之前我们已经学过如何使用mysqldump备份恢复数据库:<mysql逻辑备份与还原工具mysqldump>,就目前来说,mysqldump是使用最广泛的MyS ...

  5. mysql无列名注入

    0x00 原理   mysql无列名注入是报错注入的一个变种,前提是已知表名,但是不知道列名,或者只知道部分列名,可通过报错注入拼接查询自身表,当自表被拼接时,由于存在重复属性列,会将列信息报错返回, ...

  6. [bug] CDH 安装 Error : No matching Packages to list

    信息 分析 我的系统是CentOS 7,而 cm 安装包是配合 redhat 6 的,应该选择 redhat 7 目录下的包 参考 https://community.cloudera.com/t5/ ...

  7. 解决nohup: 忽略输入并把输出追加到"nohup.out"或者nohup: 忽略输入重定向错误到标准输出端

    nohup启动脚本的时候,没有指定输出路径,默认使用当前目录的nohup.out 例如下面这句就是默认使用nohup.out作为输出文件: nohup script.sh & 改成下面的,则/ ...

  8. ocalhost kernel: [244840.301449] nf_conntrack: nf_conntrack: table full, dropping packet

    nf_conntrack: table full, dropping packet. 终结篇   "连接跟踪表已满,开始丢包"!相信不少用iptables的同学都会见过这个错误信息 ...

  9. 查看linux系统是多少位,使用 getconf LONG_BIT

    查看linux系统是多少位,使用 getconf LONG_BIT echo $HOSTTYPE

  10. ipmitool使用手册(20200401)

    ipmitool使用手册原创xinqidian_xiao 最后发布于2018-07-05 12:15:47 阅读数 17579 收藏展开一.查找安装包 查看ipmitool属于哪个安装包 #yum p ...