图解ReentrantReadWriteLock

如果之前使用过读写锁, 那么可以直接看本篇文章. 如果之前未使用过, 那么请配合我的另一篇文章一起看:[源码分析]读写锁ReentrantReadWriteLock

0. demo

我先给出一个demo, 这样大家就可以根据我给的这段代码, 边调试边看源码了. 还是那句话: 注意"My" , 我把ReentrantReadWriteLock类 改名为了 "MyReentrantReadWriteLock"类 , "Lock"类 改名为了"MyLock"类. 大家粘贴我的代码的时候, 把相应的"My"都去掉就好了, 否则会编译报错哦.

demo里是一个公平读写锁

import java.util.HashMap;
import java.util.Map;
import java.util.Scanner;
import java.util.concurrent.locks.Lock;
import java.util.function.Supplier; public class ReentrantReadWriteLockTest2 {
static final Scanner scanner = new Scanner(System.in);
static volatile String cmd = "";
private static MyReentrantReadWriteLock lock = new MyReentrantReadWriteLock(true);
private static MyReentrantReadWriteLock.ReadLock readLock = lock.readLock();
private static MyReentrantReadWriteLock.WriteLock writeLock = lock.writeLock(); public static void main(String[] args) {
for (Map.Entry<String, Lock> entry : new HashMap<String, Lock>() {{
for (int i = 0; i < 10; i++) {
put("r" + i, readLock);
put("w" + i, writeLock);
}
}}.entrySet()) {
new Thread(() -> func(entry::getValue, entry.getKey())).start();
} while (scanner.hasNext()) {
cmd = scanner.nextLine();
}
} public static void func(Supplier<Lock> myLockSupplier, String name) {
String en_type = myLockSupplier.get().getClass().getSimpleName().toLowerCase().split("lock")[0];
String zn_type = (en_type.equals("read") ? "读" : "写");
blockUntilEquals(() -> cmd, "lock " + en_type + " " + name);
myLockSupplier.get().lock();
System.out.println(name + "获取了" + zn_type + "锁");
blockUntilEquals(() -> cmd, "unlock " + en_type + " " + name);
myLockSupplier.get().unlock();
System.out.println(name + "释放了" + zn_type + "锁");
} private static void blockUntilEquals(Supplier<String> cmdSupplier, final String expect) {
while (!cmdSupplier.get().equals(expect))
quietSleep(1000);
} private static void quietSleep(int mills) {
try {
Thread.sleep(mills);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

使用例子在下面.

我们可以看到r1持有了读锁之后, r2来申请读锁, 也可以成功. 说明读锁是可以共享的.

接下来申请写锁. 申请的写锁会进入到`等待队列`.

然后咱们再申请读锁r3, r4. 由于咱们的demo是公平的读写锁. `等待队列`中有线程在等待写锁时, 后续的申请读锁的线程也都会直接进入`等待队列`. 不会和先来的写锁线程争抢.

大致就是这样.更详细的请看下面小节的详解.

(图1)

1. 开始图解 (公平读写锁)

-----------2018.07.29 下午---更新-----关于图片不清楚的问题------------------------

本文下面评论中提出这篇博客的图片看着很不舒服. 我以后会注意的. 之前没想到显示图片会有差别.

不过这里临时先给出一个解决办法. 如果想看仔细看某一个图片, 那么请这样做(可以拿上面的图1来试试, 本篇中除了图2和图3都可以的 ):

在图片上右键 -> 新标签中打开图片 - > 在这个新标签中鼠标指针是一个放大镜, 左键点击一下, 让图片放大.然后你会发现, 图片非常非常非常清晰....

大概就是这样的区别:

(图2)

(图3)

----------------------下面咱们回到本篇主题---------------------------

咱们实例化一个读写锁后, 锁的状态大致如下图:

此时锁是空闲状态.

如果这个时候r1来申请读锁.那么就可以直接成功, 变化如下的黑色阴影部分.

firstReader 是线程的引用. 读锁是共享的, 可以有很多线程来获取读锁. 而firstReader是记录这些持有读锁线程中第一个获得读锁的线程的.

firstReaderHoldCount是 firstReader引用的线程的读锁获得次数(也就是firstReader重入的次数)

接下来如果r2来申请读锁, 会发生什么?

r2会申请成功, 而且变化如下:

其中cacheHoldCounter是一个引用, 总是指向最后一个获得读锁的线程的计数器.

接下来让w1线程申请写锁. 写锁和读锁是互斥的, 所以写锁无法申请成功, 于是会进入到`等待队列`.

由于等待队列是懒初始化, 所以这个时候才会产生等待队列的头结点:

然后就是把w1对应的Node尾插到`等待队列`中了:

然后再把当前节点的前驱节点的waitStatus置为-1.  -1表示后继节点在等待线程被激活. 

然后线程w1就放心地挂起了:

接下来咱们再让r3线程获取读锁会怎么样呢?

(咱们现在演示的是公平锁, 如果有线程在队列里等待的话, 后续申请读锁的线程就不会直接拿到读锁, 而是进入到等待队列中. 毕竟写锁先来的嘛, 不能插队.)

线程r3进入到了`等待队列`中.然后线程r3挂起了. 变化如上图的黑色阴影部分所示.

接下来咱们让r4申请读锁, 最终结果和r3一样, 就是进入到了`等待队列`的最末尾. (但是这个r4在后续的讲解中有用)

所以r4就不用讲了, 和r3一样:

接下来咱们释放r1的读锁:

然后释放r2的读锁:

(cachedHoldCounter我没有加阴影, 是因为, 他其实并不是真的变为null了, 还是指向原来的那个元素, 但是这个已经不重要了.)

当线程r2释放读锁的时候发现读锁已经被完全释放了, 所以会激活`等待队列` 里的第一个线程.

并且让第一个线程对应的Node作为新的Head. 淘汰掉原先的Head.

释放w1的写锁:

线程w1释放了读锁后, 激活了自己的后继节点r3.

r3被激活后,开始准备获取读锁.

把firstReader指向自己后, 把自己替换为新的Head节点:

线程r3申请完读锁后, 查看后继节点的nextWaiter是否等于Node.SHARED. 如果是, 那么就会唤醒这个后继节点.

所以接下来会唤醒r4:

现在r4被激活了, r4开始申请读锁了:

然后r4即将成为新的Head节点:

到这里, demo里的演示部分就完成了.

最后, 咱们依次把r3 和 r4 也都释放了吧. 反正也剩的不多了.

释放了r3的时候, 变化如下:

最后咱们释放掉r4:

(其中的cachedHoldCounter并不是真正地变为了null, 而是还在指向着原来的元素. 只是在这里显得没用了, 所以那部分没话)

线程r4执行完之后, 所有的线程就都释放了. 锁的状态如下:

到这里, 整个公平读写锁的申请锁, 释放锁的过程, 就都演示完了.

[图解Java]读写锁ReentrantReadWriteLock的更多相关文章

  1. Java并发指南10:Java 读写锁 ReentrantReadWriteLock 源码分析

    Java 读写锁 ReentrantReadWriteLock 源码分析 转自:https://www.javadoop.com/post/reentrant-read-write-lock#toc5 ...

  2. 轻松掌握java读写锁(ReentrantReadWriteLock)的实现原理

    转载:https://blog.csdn.net/yanyan19880509/article/details/52435135 前言 前面介绍了java中排它锁,共享锁的底层实现机制,本篇再进一步, ...

  3. Java读写锁(ReentrantReadWriteLock)学习

    什么是读写锁 平时,我们常见的synchronized和Reentrantlock基本上都是排他锁,这些锁在同一时刻只允许一个线程进行访问,哪怕是读操作.而读写锁是维护了一对锁(一个读锁和一个写锁), ...

  4. 源码分析— java读写锁ReentrantReadWriteLock

    前言 今天看Jraft的时候发现了很多地方都用到了读写锁,所以心血来潮想要分析以下读写锁是怎么实现的. 先上一个doc里面的例子: class CachedData { Object data; vo ...

  5. Java并发(十):读写锁ReentrantReadWriteLock

    先做总结: 1.为什么用读写锁 ReentrantReadWriteLock? 重入锁ReentrantLock是排他锁,在同一时刻仅有一个线程可以进行访问,但是在大多数场景下,大部分时间都是提供读服 ...

  6. java 可重入读写锁 ReentrantReadWriteLock 详解

    详见:http://blog.yemou.net/article/query/info/tytfjhfascvhzxcyt206 读写锁 ReadWriteLock读写锁维护了一对相关的锁,一个用于只 ...

  7. 读写锁ReentrantReadWriteLock:读读共享,读写互斥,写写互斥

    介绍 DK1.5之后,提供了读写锁ReentrantReadWriteLock,读写锁维护了一对锁:一个读锁,一个写锁.通过分离读锁和写锁,使得并发性相比一般的排他锁有了很大提升.在读多写少的情况下, ...

  8. Java读写锁

    Java读写锁,ReadWriteLock.java接口, RentrantReadWriteLock.java实现.通过读写锁,可以实现读-读线程并发,读-写,写-读线程互斥进行.以前面试遇到一个问 ...

  9. 从火车站车次公示栏来学Java读写锁

    Java多线程并发之读写锁 本文主要内容:读写锁的理论:通过生活中例子来理解读写锁:读写锁的代码演示:读写锁总结.通过理论(总结)-例子-代码-然后再次总结,这四个步骤来让大家对读写锁的深刻理解. 本 ...

随机推荐

  1. java的设计模式 - 外观模式(Facade)

    目的 看脸模式目的很简单,就是给用户留个好印象,不想让用户关注系统中的具体细节,关注系统的外表(暴露出来的接口)就好了.一些 GUI 的菜单也好,SDK 也好或多或少也会用到这种思想.这更多的是一种思 ...

  2. 下载合适的tomcat版本

    Tomcat因技术先进.性能稳定,而且免费,因而深受Java爱好者的喜爱并得到了部分软件开发商的认可,成为目前比较流行的Web 应用服务器.对于新手来讲,如何下载合适的tomcat版本呢?今天我们以下 ...

  3. 爬虫技术实现空间相册采集器V.0.0.1版本

    一.    功能需求分析: 在很多时候我们需要做这样一个事情:我们想把我们QQ空间上的相册高清图像下载下来,怎么做?到网上找软件?答案是否定的,理由之一:网上很多软件不知有没有病毒,第二它有可能捆了很 ...

  4. U盘启动盘安装Windows10操作系统详解

    没有装过系统的同学,总以为装系统很神秘?是专业技术人员干的事情.今天我们来看看怎么借助常用的U盘装上全新的win10系统. 准备材料: 软件软碟通,可上官网下载:https://cn.ultraiso ...

  5. git之命令git checkout

    git checkout 最常用的就是切换分支,最近又发现一种新的用法: 有时候,在看代码的时候,不小心改动了部分代码,但跟项目没啥关系,这个时候,想不去提交这些代码,怎么处理呢? 使用git che ...

  6. eval、exec及元类、单例实现的5种方法

    eval内置函数 # eval内置函数的使用场景:#   1.执行字符串会得到相应的执行结果#   2.一般用于类型转化,该函数执行完有返回值,得到dict.list.tuple等​dic_str = ...

  7. 001_Go hello world

    一.go获取程序参数及指针地址示例 package main import ( "fmt" "os" ) func main() { fmt.Println(o ...

  8. UI自动化测试之Jenkins配置

    前一段时间帮助团队搭建了UI自动化环境,这里将Jenkins环境的一些配置分享给大家. 背景: 团队下半年的目标之一是实现自动化测试,这里要吐槽一下,之前开发的测试平台了,最初的目的是用来做接口自动化 ...

  9. PHP按权重随机

    之前业务部门提了一个需求,要求将广告按照ecpm高低进行随机.(即:ecpm高的获取流量的几率大) 如下数组: //要求AD1的概率要求为50%,AD2概率为25% ,AD3的概率为15%,AD4的概 ...

  10. Index-技术学习系列博客

    计算机理论基础系列 B树的插入操作 前端系列 安装nodejs和webpack环境 构建vue项目 Json Web Token VO和DO转换(一) 工具汇总 缓存的实现和使用 框架学习系列 shi ...