之前讲了《AQS源码阅读》《ReentrantLock源码阅读》,本次将延续阅读下ReentrantReadWriteLock,建议没看过之前两篇文章的,先大概了解下,有些内容会基于之前的基础上阅读。

这个并不是ReentrantLock简单的升级,而是落地场景的优化,我们来详细了解下吧。

背景

JUC包里面已经有一个ReentrantLock了,为何还需要一个ReentrantReadWriteLock呢?看看头注解找点线索。



它是ReadWriteLock接口的实现。那看看这个接口怎么说



在实际场景中,一般来说,读数据远比写数据要多。如果我们还是用独占锁去锁线程避免线程不安全的话,是非常低效的,而且同时也会失去它的并发性。多线程也没有意义了。所以ReadWriteLock就是解决这个问题所存在的。

看回ReentrantReadWriteLock的头注解。



ReentrantReadWriteLock依然有公平锁/非公平锁的功能,与ReentrantLock不同在于,前者内部维护了读锁和写锁,在公平/非公平模式下,他们会一起去竞争这个锁资源。



上图是两条ReentrantReadWriteLock最核心的规则。

  1. 申请读锁。当没有其他写锁占有,或者读锁在队列中排队时间最长的,才能成功
  2. 申请写锁。当没有其他线程占有读/写锁的情况下,才能成功

又以上两条规则可以推导出,

  1. 写锁比读锁要高级
  2. 有读锁占用可以继续申请读锁,但其他线程不能申请写锁
  3. 有写锁占用其他线程读写都不能申请

所以扣ReadWriteLock接口的说明,可以让读并发,写独占,提高了程序的并发性。

ReentrantReadWriteLock构成

看下ReentrantReadWriteLock的file struture



之前看过ReentrantLock源码的同学肯定很熟悉这个结构,看起来相同的都是Sync同步器(AQS的子类),以及它的两个公平/非公平子类。

不同的是它还多了ReadLock内部类和WriteLock内部类,以及读写对应的成员变量和方法。并且少了lock()、unlock()等方法,而是把加锁解锁的功能下方给这两个子类,符合ReadWriteLock接口的定义。

Sync内部类

虽然ReentrantReadWriteLock和ReentrantLock都有Sync,但其实Sync方法已经很大不同了,看下Sync的结构



对比之前ReentrantLock的Sync,最大不同在于它多了**shared()方法,用于共享锁的获取与释放。

另外tryReadLock()、tryWriteLock()是给WriteLock和ReadLock内部类使用的。

tryAcquire() 独占锁(写锁)申请



上文介绍重入锁说到state代表的是重入的次数,在读写锁的语义下,state代表的读/写占有(重入)的次数。c为state,w为独占重入次数。

当有线程占用锁时(c!=0),如果没有写锁(w==0)或者独占线程不是当前线程,返回false获取失败。锁的重入总数超过上限会抛出异常。

这里很容易看出来,如果有锁占用的时候,如果只是读锁,依然可以申请成功。这就是读锁的锁升级

当没有线程占用的时候,执行writerShouldBlock()判断是否需要阻塞线程(子类实现自己的条件),不需要则CAS state值,返回成功。

tryAquireShared() 共享锁(读锁)申请



读锁申请比写锁申请要复杂,有比较多没接触过的成员变量,判断的语句也比较多。

先看看成员变量,从他们各自的变量注解可知

  • firstReader,是第一个获取读锁的线程
  • firstReaderHoldCount,是firstReader的计数器。
  • cachedHoldCounter,最近一个成功获取读锁的线程持有数计数器。
  • readHolds,当前线程重入读锁次数。ThreadLocal,是线程安全的HoldCounter

先判断是否有写锁占有,如果写锁不是当前线程,获取读锁失败,退出方法。

注意如果写锁是当前线程是可以获取读锁的,因为写锁是独占的,这种情况下是不会有数据与其他线程共享的问题。

满足子类条件,也不超过总数,CAS也成功的情况下,

如果没有读锁,则设firstReader为当前线程,firstReaderHoldCount为1;

如果有读锁,并且也是当前线程申请获取,firstReaderHoldCount自增1;

如果有读锁,不是当前线程申请,取上一个成功的缓存计数器,如果这个计数器不是当前线程的,则设为当前的计数器,并且自增,返回成功。(其实就是把缓存计数器置换为当前线程的计数器)

最后不满足条件或者CAS失败,执行fullTryAcquireShared(current)返回。

至于这些数据算来干嘛,等后面看看release()怎么用。



其实这个方法就是用for循环轮询解决CAS丢失和重入失败的问题,具体代码不细过了,有兴趣可以自己找源码看看。

tryRelease() 独占锁(写锁)释放



这里又有Condition的踪迹了,大概可以才行到Condition时控制锁的行为的,取消唤醒等操作。

另外锁会同时释放读锁和写锁。

这个方法比较好理解的,只要是当前线程操作下,持有重入数减去释放数为0就可以释放了,否则失败。

tryReleaseShared() 共享锁(读锁)释放



释放读锁,对正在读的线程不会有什么影响,但可以让等待的写线程去开始获取写锁。

剩余的内容就是对tryAquireShared()计算的count数值进行释放(自减),如果最终自减为0则释放读锁成功。

WriteLock、ReadLock内部类

前面说到ReentrantReadWriteLock的lock()、unlock()操作是分配到Write/ReadLock里面执行的。

他们都是Lock接口的实现,所以其实最像ReentrantLock应该是这个两个内部类。而且大体上也没什么差异,也是用Sync的内部类。

WriteLock、ReadLock最大的不同就是WriteLock用的独占模式的方法,ReadLock用的是共享模式的方法。

具体的代码实现基本就是上面说明的组成,下面介绍下ReentranReadWriteLock的使用。

ReentrantLock的时候比较简单,声明一个变量,调用lock()方法即可。

    ReentrantLock rl = new ReentrantLock();
rl.lock();
rl.unlock();

但ReentranReadWriteLock并不是Lock接口的实现,所以没有这些方法。

有的只是writeLock()、readLock(),要先调用这个方法获取应对的锁对象,再调用lock()。

     ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
rwl.readLock().lock();
rwl.readLock().unlock();
rwl.writeLock().lock();
rwl.writeLock().unlock();

总结

回顾下要点

  1. 读写锁ReentrantReadWriteLock,是基于多读少写的实际场景,提高并发性
  2. 读写锁的Sync添加了共享模式的方法
  3. 读写锁内置了两个对象readLock、writeLock,用于实际的加锁解锁
  4. 写锁是独占的,不允许其他锁的申请
  5. 读锁可以并发重复申请,当有写锁的时候,会发生锁升级

特别地,在此祝福8月27日生日的她。


更多技术文章、精彩干货,请关注

博客:zackku.com

微信公众号:Zack说码

Java多线程——ReentrantReadWriteLock源码阅读的更多相关文章

  1. Java多线程——ReentrantLock源码阅读

    上一章<AQS源码阅读>讲了AQS框架,这次讲讲它的应用类(注意不是子类实现,待会细讲). ReentrantLock,顾名思义重入锁,但什么是重入,这个锁到底是怎样的,我们来看看类的注解 ...

  2. Java多线程框架源码阅读之---ReentrantLock

    ReentrantLock基于Sync内部类来完成锁.Sync有两个不同的子类NonfairSync和FairSync.Sync继承于AbstractQueuedSynchronizer. Reent ...

  3. [Java多线程]-ThreadLocal源码及原理的深入分析

    ThreadLocal<T>类:以空间换时间提供一种多线程更快捷访问变量的方式.这种方式不存在竞争,所以也不存在并发的安全性问题. //-------------------------- ...

  4. java.util.HashSet, java.util.LinkedHashMap, java.util.IdentityHashMap 源码阅读 (JDK 1.8)

    一.java.util.HashSet 1.1 HashSet集成结构 1.2 java.util.HashSet属性 private transient HashMap<E,Object> ...

  5. java.util.HashSet, java.util.LinkedHashMap, java.util.IdentityHashMap 源码阅读 (JDK 1.8.0_111)

    一.java.util.HashSet 1.1 HashSet集成结构 1.2 java.util.HashSet属性 private transient HashMap<E,Object> ...

  6. java多线程---ReentrantLock源码分析

    ReentrantLock源码分析 基础知识复习 synchronized和lock的区别 synchronized是非公平锁,无法保证线程按照申请锁的顺序获得锁,而Lock锁提供了可选参数,可以配置 ...

  7. 【JAVA】HashMap源码阅读

    目录 1.关键的几个static参数 2.内部类定义Node节点 3.成员变量 4.静态方法 5.HashMap的四个构造方法 6.put方法 7.扩容resize方法 8.get方法 9.remov ...

  8. java.nio.Buffer源码阅读

    Java 自从 JDK1.4 起,对各种 I/O 操作使用了 Buffer 和 Channel 技术.这种更接近于操作系统的的底层操作使得 I/O 操作速度得到大幅度提升,下面引用一段<Java ...

  9. Java的Vector源码阅读

    * The {@code Vector} class implements a growable array of * objects. Like an array, it contains comp ...

随机推荐

  1. xmlns:sys="clr-namespace:System;assembly=mscorlib" NOTE: System;与assembly中间不能有空格

    xmlns:sys="clr-namespace:System;assembly=mscorlib"  NOTE: System;与assembly中间不能有空格 否则报错, Er ...

  2. js介绍自己的例子

    js并不是真正面向对象的语言,但是我们通过一些方法也是可以实现js的一些面向对象设计的.常见的构造函数有很多模式有构造函数模式,原型链,工厂模式等等.但就是因为,我初学者看起来非常吃力,理解起来都是很 ...

  3. 转载:Apache commons开源工具简介

    Apache Commons是一个非常有用的工具包,解决各种实际的通用问题,下面是一个简述表,详细信息访问http://jakarta.apache.org/commons/index.html Be ...

  4. LOJ tangjz的背包

    题目大意 有 \(n\) 个物品, 第 \(i\) 个物品的体积为 \(i\) 令 \(f(x)\) 为 选择 \(m\) 个物品, 体积和为 \(x\) 的方案数 令 \(V = \sum_{i=1 ...

  5. [POJ1144][BZOJ2730]tarjan求割点

    求割点 一种显然的n^2做法: 枚举每个点,去掉该点连出的边,然后判断整个图是否联通 用tarjan求割点: 分情况讨论 如果是root的话,其为割点当且仅当下方有两棵及以上的子树 其他情况 设当前节 ...

  6. [POJ2406&POJ1961]用KMP解决字符串的循环问题两例

    翻阅了一下网上资料,发现大部分都说这题是找规律...或是说YY出的一个算法..不会证明... 然后就脑补了一下证明 ~ 结论:对于一个字符串S[1..N],如果N mod (N-next[N])=0 ...

  7. 单源最短路模板_SPFA_Dijkstra(堆优化)_C++

    随手一打就是标准的SPFA,默认1号节点为出发点,当然不用 f 判断是否在队里也可以,只是这样更优化一点 void spfa() { int i,x,k; ;i<=n;i++) { d[i]=o ...

  8. Page.Response.Buffer与Response.Redirect一起用报错“无法在发送 HTTP 标头之后进行重定向”

    Page.Response.Buffer与Response.Redirect一起用报错“无法在发送 HTTP 标头之后进行重定向” 原因还未知..

  9. Oracle基础 07 参数文件 pfile/spfile

    --查看数据库运行模式(spfile还是pfile)select decode(count(*),1,'spfile','pfile') from v$spparameterwhere rownum= ...

  10. [ 手记 ] 联想rd650服务器整列及系统安装

    联想 RD650服务器 磁盘阵列:http://wenku.baidu.com/view/b364c2db5f0e7cd185253644.html?from=search 该服务器安装系统需要BIO ...