转载:https://blog.csdn.net/yanyan19880509/article/details/52435135

前言

前面介绍了java中排它锁,共享锁的底层实现机制,本篇再进一步,学习非常有用的读写锁。鉴于读写锁比其他的锁要复杂,不想堆一大波的文字,本篇会试图图解式说明,把读写锁的机制用另外一种方式阐述,鉴于本人水平有限,如果哪里有误,请不吝赐教。

公平读写锁

ReentrantReadWriteLock的锁策略有两种,分为公平策略和非公平策略,两者有些小区别,为便于理解,本小节将以示例的形式来说明多线程下,使用公平策略的读写锁是如何处理的。

首先看一下即将出场的伙伴们,我们一共会出场几个线程,还有用于实现读写机制的AQS同步器队列。每个线程中的 R(0)W(0)表示当前线程占用了多少读写锁。

接下来,我们一步步来看在公平策略下多线程并发的读写机制是怎样的。

1.线程A请求一个读锁,此时无人竞争锁,A获取读锁1,即线程A重入次数为1,如下所示:

2.线程B请求一个读锁,由于AQS中没有等待节点,当前处于读锁占有状态(线程A占有1个读锁),所以B成功获取读锁,如下所示:

3.这时候,线程C请求一个写锁,由于当前其他两个线程拥有读锁,写锁获取失败,线程C入队列,如下所示:

AQS初始化会创建一个空的头节点,C入队列,然后会休眠,等待其他线程释放锁唤醒。

4.线程D也来了,线程D想获取一个读锁,虽然当于处于读锁占有阶段,但是目前D不占有任何数量的读锁,而且同步器队列中已经有等待节点,这时候,由于公平策略,D不得已,一个字,等,如下图所示:

5.这时候,线程A执行完了,释放了读锁,由于B仍然占有读锁,所以释放后读锁仍然没有完全释放,写锁仍然没有机会执行,如下图所示:

6.这次,B也执行完了,执行完后,读锁全部释放,这时候会唤醒排在同步器队头的节点C,C成功获取一个写锁,如下图所示:

7.一旦任何一个线程获取了写锁,除了该线程自己,其它线程都将无法获取读锁和写锁,这时候,线程C再次请求一个读锁,这是允许的,但反过来如果一个线程先获取了读锁,再获取写法则是不行的。这时候的状态如下图所示:

8.这时候假设线程E也来了,E想获取读锁,由于当前处于写锁状态,直接入队,如下所示:

9.这会C终于把活干完了,把读锁和写锁都给释放了,然后线程D被唤醒,获取了读锁,如下图所示:

10.这时候,如果再来一个线程,比如A,也想获取读锁,由于节点中还有线程E在等待,而且当前线程A没有获取任何读锁,不是重入状态,所以只能置入队尾,如下图所示:

11.这时候,如果D再次调用了一次获取读锁,由于D属于可重入状态,所以直接把读锁+1即可,如下图所示:

12.由于D获取的是读锁,同步队列中的E等待的也是读锁,所以E会被唤醒,获取读锁继续执行,如下图所示:

13.同样的,由于线程A获取的是读锁,在E执行后,会唤醒线程A,A也可以获得读锁,并继续执行,如下图所示:

14.最后大家各自执行,悄然退场。

非公平读写锁

接下来我们再来看一下非公平策略读写锁机制又是如何的,为了更好的对比,我们沿用公平锁的流程。

由于获取读锁的逻辑比较复杂,我们在这里先简单进行归纳:

a. 如果当前全局处于无锁状态,则当前线程获取读锁

b. 如果当前全局处于读锁状态,且队列中没有等待线程,则当前线程获取读锁

c. 如果当前全局处于写锁占用状态(并且不是当前线程占有),则当前线程入队尾

d. 如果当前全局处于读锁状态,且等待队列中第一个等待线程想获取写锁,那么当前线程能够获取到读锁的条件为:当前线程获取了写锁,还未释放;当前线程获取了读锁,这一次只是重入读锁而已;其它情况当前线程入队尾。之所以这样处理一方面是为了效率,一方面是为了避免想获取写锁的线程饥饿,老是得不到执行的机会

e. 如果当前全局处于读锁状态,且等待队列中第一个等待线程不是写锁,则当前线程可以抢占读锁

获取写锁相对就比较简单了,规则如下:

h. 如果当前处于无锁状态,则当前线程获取写锁

i. 如果当前全局处于读锁状态,当前线程入队尾

j. 如果当前全局处于写锁状态,除非是重入获取写锁,否则入队尾

接下来我们看一遍流程:

1.线程A请求一个读锁,全局处于无锁状态,根据规则a,线程A获取了锁,如下图所示:

2.线程B请求一个读锁,根据规则b,线程B可以获取到读锁

3.这时候,线程C请求一个写锁,由于当前其他两个线程拥有读锁,写锁获取失败,线程C入队列(根据规则i),如下所示:

AQS初始化会创建一个空的头节点,C入队列,然后会休眠,等待其他线程释放锁唤醒。

4.线程D也来了,线程D想获取一个读锁,根据读锁规则d,队列中第一个等待线程C请求的是写锁,为避免写锁迟迟获取不到,并且线程D不是重入获取读锁,所以线程D也入队,如下图所示:

5.这时候,线程A执行完了,释放了读锁,由于B仍然占有读锁,所以释放后读锁仍然没有完全释放,写锁仍然没有机会执行,如下图所示:

6.这次,B也执行完了,执行完后,读锁全部释放,这时候会唤醒排在同步器队头的节点C,C成功获取一个写锁,如下图所示:

7.一旦任何一个线程获取了写锁,除了该线程自己,其它线程都将无法获取读锁和写锁,这时候,线程C再次请求一个读锁,这是允许的,但反过来如果一个线程先获取了读锁,再获取写锁则是不行的。这时候的状态如下图所示:

8.这时候假设线程E也来了,E想获取读锁,由于当前处于写锁状态,直接入队,如下所示:

9.这会C终于把活干完了,把读锁和写锁都给释放了,然后线程D被唤醒,获取了读锁,如下图所示:

10.这时候,如果再来一个线程,比如A,也想获取读锁,虽然等待队列中,E线程刚好还没被唤醒,但A线程是可以抢占读锁的(这里假设抢占到了),这个跟公平锁有明显的区别,如下图所示:

11.这时候,如果D再次调用了一次获取读锁,由于D属于可重入状态,所以直接把读锁+1即可,如下图所示:

12.由于当前状态下处于读锁状态,前面的线程D其实醒来后,是会同时唤醒线程E的,所以线程E也醒过来继续干活了,如下图所示:

13.同步队列中没有等待线程了,各个线程执行完后,一切相安无事了。

总结

考虑到业务的多样化,java5中提供的并发包中的工具类大部分都同时提供了公平及非公平策略,这两种策略下,一般而言,非公平锁吞吐会比较大,所以默认情况下都是使用的非公平策略。

本篇试图以尽量简单的方式来阐明读写锁的实现机制,为了直观,我们只考虑简单抽象的方式,实际在实现的时候,会使用CAS去竞争锁。特别是在非公平策略中的第10个步骤,这种情况下有可能E先获取了读锁。很多时候,我们在大致了解了实现步骤,流程之后,再去品味源码,就会更加的轻松。

最后还是建议大家在了解了思路之后,自己多看看源码,多思考,学到的才是属于自己的东西。

轻松掌握java读写锁(ReentrantReadWriteLock)的实现原理的更多相关文章

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

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

  2. [图解Java]读写锁ReentrantReadWriteLock

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

  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. Spark操作—aggregate、aggregateByKey详解

    https://blog.csdn.net/u013514928/article/details/56680825 1. aggregate函数 将每个分区里面的元素进行聚合,然后用combine函数 ...

  2. Docker 安装Hadoop集群

    资源准备:jdk1.8及hadoop2.7.3 链接:https://pan.baidu.com/s/1x8t1t2iY46jKkvNUBHZlGQ 提取码:g1gm 复制这段内容后打开百度网盘手机A ...

  3. APUE第八章-进程控制

    一.进程标识 二.函数fork 1.写时复制,copy-on-write 2.文件共享,父进程等待子进程完成,子进程结束后,它对任一共享描述符的读写操作的文件偏移量已做相应的更新,同时操作时,可以考虑 ...

  4. C++_类和动态内存分配2-改进后的String类

    添加前面介绍过的复制构造函数和赋值运算符,使类能够正确管理类对象使用的内存. 知道对象何时被创建和释放. =================================== 修订后的默认构造函数 ...

  5. rest-assured的对象映射(序列化和反序列化)

    rest-assured支持映射Java对象到Json和XML以及从Json和XML中映射到Java对象.Json映射需要在classpath 中有Jackson.Jackson 2或者是Gson,X ...

  6. URL 地址传中文,转码

    地址传中文,转码 http://community.csdn.net/Expert/topic/4541/4541877.xml?temp=.57688541.设置web.config文件.<s ...

  7. 获取HTML代码用 像阿里巴巴

         public string GetHtml(string url)         {               string html = String .Empty;          ...

  8. bzoj1079 着色方案 记忆化搜索(dp)

    题目传送门 题目大意: 有k种颜色,每个颜色ci可以涂个格子,要求相邻格子颜色不能一样,求方案数.ci<=5,k<=15. 思路: 题目里最重要的限制条件是相邻格子颜色不能相同,也就是当前 ...

  9. springboot(五)-使用Redis

    Redis服务器 springboot要使用redis,首先当然要确保redis服务器能够正常跑起来. pom.xml 这里添加redis的依赖,当然也是springboot集成好的. <!-- ...

  10. PHP验证身份信息

    $code = 'code'; $patt = "/^[1-9]\d{5}(19|20)\d{2}((0[1-9])|(10|11|12))([012](\d|(30|31)))\d{3}[ ...