资讯专栏INFORMATION COLUMN

Java多线程—ReentrantReadWriteLock源码阅读

Ververica / 1735人阅读

摘要:不同的是它还多了内部类和内部类,以及读写对应的成员变量和方法。另外是给和内部类使用的。内部类前面说到的操作是分配到里面执行的。他们都是接口的实现,所以其实最像应该是这个两个内部类。而且大体上也没什么差异,也是用的内部类。

之前讲了《AQS源码阅读》和《ReentrantLock源码阅读》,本次将延续阅读下ReentrantReadWriteLock,建议没看过之前两篇文章的,先大概了解下,有些内容会基于之前的基础上阅读。
这个并不是ReentrantLock简单的升级,而是落地场景的优化,我们来详细了解下吧。

背景

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

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

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

看回ReentrantReadWriteLock的头注解。

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

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

申请读锁。当没有其他写锁占有,或者读锁在队列中排队时间最长的,才能成功

申请写锁。当没有其他线程占有读/写锁的情况下,才能成功

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

写锁比读锁要高级

有读锁占用可以继续申请读锁,但其他线程不能申请写锁

有写锁占用其他线程读写都不能申请

所以扣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();
总结

回顾下要点

读写锁ReentrantReadWriteLock,是基于多读少写的实际场景,提高并发性

读写锁的Sync添加了共享模式的方法

读写锁内置了两个对象readLock、writeLock,用于实际的加锁解锁

写锁是独占的,不允许其他锁的申请

读锁可以并发重复申请,当有写锁的时候,会发生锁升级

如果觉得还不错,请关注公众号:Zack说码

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/77064.html

相关文章

  • Java线程进阶(十)—— J.U.C之locks框架:基于AQS的读写锁(5)

    摘要:关于,最后有两点规律需要注意当的等待队列队首结点是共享结点,说明当前写锁被占用,当写锁释放时,会以传播的方式唤醒头结点之后紧邻的各个共享结点。当的等待队列队首结点是独占结点,说明当前读锁被使用,当读锁释放归零后,会唤醒队首的独占结点。 showImg(https://segmentfault.com/img/remote/1460000016012293); 本文首发于一世流云的专栏:...

    dunizb 评论0 收藏0
  • Lock锁子类了解一下

    摘要:前言回顾前面多线程三分钟就可以入个门了源码剖析多线程基础必要知识点看了学习多线程事半功倍锁机制了解一下简简单单过一遍只有光头才能变强上一篇已经将锁的基础简单地过了一遍了,因此本篇主要是讲解锁主要的两个子类那么接下来我们就开始吧一锁首先我们来 前言 回顾前面: 多线程三分钟就可以入个门了! Thread源码剖析 多线程基础必要知识点!看了学习多线程事半功倍 Java锁机制了解一下 AQ...

    时飞 评论0 收藏0
  • AQS同步组件--ReentrantLock与锁

    摘要:性能较好是因为避免了线程进入内核的阻塞状态请求总数同时并发执行的线程数我们首先使用声明一个所得实例,然后使用进行加锁和解锁操作。 ReentrantLock与锁 Synchronized和ReentrantLock异同 可重入性:两者都具有可重入性 锁的实现:Synchronized是依赖jvm实现的,ReentrantLock是jdk实现的。(我们可以理解为一个是操作系统层面的实现...

    dcr309duan 评论0 收藏0
  • Java线程进阶(四)—— J.U.C之locks框架:ReentrantReadWriteLoc

    摘要:我们知道,的作用其实是对类的和的增强,是为了让线程在指定对象上等待,是一种线程之间进行协调的工具。当线程调用对象的方法时,必须拿到和这个对象关联的锁。 showImg(https://segmentfault.com/img/remote/1460000016012566); 本文首发于一世流云的专栏:https://segmentfault.com/blog... 一、Reentr...

    kumfo 评论0 收藏0
  • 再次认识ReentrantReadWriteLock读写锁

    摘要:但是不管怎样,在一个线程已经获取锁后,在释放前再次获取锁是一个合理的需求,而且并不生硬。那么如果考虑重入,也很简单,在加锁时将的值累加即可,表示同一个线程重入此锁的次数,当归零,即表示释放完毕。 前言 最近研究了一下juc包的源码。在研究ReentrantReadWriteLock读写锁的时候,对于其中一些细节的思考和处理以及关于提升效率的设计感到折服,难以遏制想要分享这份心得的念头,...

    miya 评论0 收藏0

发表评论

0条评论

Ververica

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<