转载: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. P4855 MloVtry的idea

    $ \color{#0066ff}{ 题目描述 }$ MloVtry是一个脑洞很大的人,它总会想出一些奇奇怪怪的idea. 可问题是,MloVtry作为一个蒟蒻,很多时候都没办法解决自己提出的问题,所 ...

  2. eclipse中查看java源码时,出现source not found问题

  3. 关于使用self.title文字不居中的解决办法

    最放发现,使用Segue在对视图切换,左上角的一般都是<Back 的一个Button控键或者是上一个视图的<title .因为上一个视图的title名字太长,导致当前视图的title被挤压 ...

  4. Wormholes 虫洞 BZOJ 1715 spfa判断负环

    John在他的农场中闲逛时发现了许多虫洞.虫洞可以看作一条十分奇特的有向边,并可以使你返回到过去的一个时刻(相对你进入虫洞之前).John的每个农场有M条小路(无向边)连接着N (从1..N标号)块地 ...

  5. 12.谈谈this对象的理解

    1.谈谈this对象的理解? 2.this指向问题   Javascript理解this对象 this是函数运行时自动生成的一个内部对象,只能在函数内部使用,但总指向调用它的对象. 通过以下几个例子加 ...

  6. Flask之flask-script 指定端口

    简介 Flask-Scropt插件为在Flask里编写额外的脚本提供了支持.这包括运行一个开发服务器,一个定制的Python命令行,用于执行初始化数据库.定时任务和其他属于web应用之外的命令行任务的 ...

  7. 118th LeetCode Weekly Contest Powerful Integers

    Given two non-negative integers x and y, an integer is powerful if it is equal to x^i + y^j for some ...

  8. POJ - 1080 枚举 / DP

    要求max{F/P},先枚举下界lowf,再贪心求符合约束条件的n个最小价值和 记录F的离散值和去重可以大幅度常数优化 (本来想着用DP做的) (辣鸡POJ连auto都Complie Error) # ...

  9. hdu 1754 I Hate It 线段树基础题

    Problem Description 很多学校流行一种比较的习惯.老师们很喜欢询问,从某某到某某当中,分数最高的是多少. 这让很多学生很反感. 不管你喜不喜欢,现在需要你做的是,就是按照老师的要求, ...

  10. vue修改组件样式

    .el-date-editor /deep/ input{ padding-left:30px; } 改变引入的组件里面元素的样式: 1.去掉css内的scoped,但是这样会污染全局 2.加上/de ...