Java并发原理抽丝剥茧,读写锁ReadWriteLock实现深入剖析

读写锁性质3

  • 如果我们要获取读锁则需要满足两个条件:目前没有线程持有写锁和目前没有线程请求获取写锁。

读写锁性质4

  • 如果我们要获取写锁则需要满足两个条件:目前没有线程持有写锁和目前没有线程持有读锁。

读写锁性质5

简单的实现版本

为了加深对读写锁的理解,在分析JDK实现的读写锁之前我们先来看一个简单的读写锁实现版本。其中三个整型变量分别表示持有读锁的线程数、持有写锁的线程数以及请求获取写锁的线程数,四个方法分别对应读锁、写锁的获取和释放操作。acquireReadLock方法用于获取读锁,如果持有写锁的线程数量或请求读锁的线程数大于0则让线程进入等待状态。releaseReadLock方法用于释放读锁,将读锁线程数减一并唤醒其它线程。acquireWriteLock方法用于获取写锁,如果持有读锁的线程数量或持有写锁的线程数量大于0则让线程进入等待状态。releaseWriteLock方法用于释放写锁,将写锁线程数减一并唤醒其它线程。

读写锁简单版本

读锁升级为写锁

在某些场景下,我们希望某个已经拥有读锁的线程能够获得写锁,并将原来的读锁释放掉,这种情况就涉及到读锁升级为写锁操作。读写锁的升级操作需要满足一定的条件,这个条件就是某个线程必须是唯一拥有读锁的线程,否则将无法成功升级。如下图中,线程二已经持有读锁了,而且它是唯一的一个持有读锁的线程,所以它可以成功获得写锁。

读锁升级

写锁降级为读锁

与锁升级相对应的是锁降级,锁降级就是某个已经拥有写锁的线程希望能够获得读锁,并将原来的写锁释放掉。锁降级操作几乎没有什么风险,因为写锁是独占锁,持有写锁的线程肯定是唯一的,而且读锁也肯定不存在持有线程,所以写锁可以直接降级为读锁。如下图中,线程三持有写锁,此时其它线程不可能持有读锁和写锁,所以可以安全地将写锁降为读锁。

写锁降级

ReadWriteLock接口

ReadWriteLock实际上是一个接口,它仅仅提供了两个方法:readLock和writeLock。分别表示获取读锁对象和获取写锁对象,JDK为我们提供了一个内置的读写锁工具,那就是ReentrantReadWriteLock类,我们将对其进行深入分析。ReentrantReadWriteLock类包含的属性和方法较多,为了让分析思路清晰且方便读者理解,我们将剔除非核心源码,只对核心功能进行分析。

ReentrantReadWriteLock三要素

ReentrantReadWriteLock类的三要素为:公平/非公平模式、读锁对象和写锁对象。其中公平/非公平模式表示多个线程同时去获取锁时是否按照先到先得的顺序获得锁,如果是则为公平模式,否则为非公平模式。读锁对象负责实现读锁功能,而写锁对象负责实现写锁功能,这两个类都属于ReentrantReadWriteLock的内部类,下面会详细讲解。

ReentrantReadWriteLock实现思想

总的来说,ReentrantReadWriteLock类的内部包含了ReadLock内部类和WriteLock内部类,分别对应读锁和写锁,这两种锁都提供了公平模式和非公平模式。不管公平模式还是非公平模式、不管是读锁还是写锁都是基于AQS同步器来实现的。实现的主要难点在于只使用一个AQS同步器对象来实现读锁和写锁,这就要求读锁和写锁共用同一个共享状态变量,下面会具体讲解如何用一个状态变量来供读锁和写锁使用。

实现思想

对应ReentrantReadWriteLock类的结构如下,ReentrantReadWriteLock.ReadLock和ReentrantReadWriteLock.WriteLock分别为读锁对象和写锁对象。Sync对象表示ReentrantReadWriteLock类的同步器,它基于AQS同步器,而FairSync类和NonfairSync类分别表示公平模式和非公平模式的同步器,可以看到默认情况下使用的是非公平模式。

读写锁共用状态变量

前面提到过ReentrantReadWriteLock的难点在于读锁和写锁都共用一个共享变量,下面看具体是如何共用的。我们知道AQS同步器的共享状态是整型的,即32位,那么最简单的共用方式就是读锁和写锁分别使用16位。其中高16位用于读锁的状态,而低16位则用于写锁的状态,这样便达到共用效果。但是这样设计后当我们要获取读锁和写锁的状态值时则需要一些额外的计算,比如一些移位和逻辑与操作。

共用状态变量

ReentrantReadWriteLock的同步器共用状态变量的逻辑如下,其中SHARED_SHIFT表示移动的位数为16;SHARED_UNIT表示读锁每次加锁对应的状态值大小,1左移16位刚好对应高16位的1;MAX_COUNT表示读锁能被加锁的最大次数,值为16个1(二进制);EXCLUSIVE_MASK表示写锁的掩码,值为16个1(二进制)。sharedCount方法用于获取读锁(高16位)的状态值,左移16位即能得到。exclusiveCount方法用于获取写锁(低16位)的状态值,通过掩码即能得到。

ReadLock与WriteLock简介

ReadLock与WriteLock是ReentrantReadWriteLock的两个要素,它们都属于ReentrantReadWriteLock的内部类。它们都实现了Lock接口,我们主要关注lock、unlock和newCondition这几个核心方法。分别表示对读锁和写锁的加锁操作、释放锁操作和创建Condition对象操作,可以看到这些方法都间接调用了ReentrantReadWriteLock的同步器的方法,需要注意的是读锁不支持创建Condition对象。我们在可重入锁ReentrantLock章节中已经讲解过Condition对象,本节将不再赘述。

公平/非公平模式

ReentrantReadWriteLock的默认模式为非公平模式,其内部类Sync是公平模式FairSync类和非公平模式NonfairSync类的抽象父类。因为ReentrantReadWriteLock的读锁使用了共享模式,而写锁使用了独占模式,所以该父类将不同模式下的公平机制抽象成readerShouldBlock和writerShouldBlock两个抽象方法,然后子类就可以各自实现不同的公平模式。换句话说,ReentrantReadWriteLock的公平机制就由这两个方法来决定了。

下面看公平模式的FairSync类,该类的readerShouldBlock和writerShouldBlock两个方法都直接返回hasQueuedPredecessors方法的结果,这个方法是AQS同步器的方法,用于判断当前线程前面是否有排队的线程。如果有排队队列就要让当前线程也加入排队队列中,这样按照队列顺序获取锁也就保证了公平性。

继续看非公平模式NonfairSync类,该类的writerShouldBlock方法直接返回false,表明不要让当前线程进入排队队列中,直接进行锁的获取竞争。readerShouldBlock方法则调用apparentlyFirstQueuedIsExclusive方法,这个方法是AQS同步器的方法,用于判断头结点的下一个节点线程是否在请求获取独占锁(写锁)。如果是则让其它线程先获取写锁,而自己则乖乖去排队。如果不是则说明下一个节点线程是请求共享锁(读锁),此时直接与之竞争读锁。

公平/非公平

写锁WriteLock的实现

上面的介绍中我们知道WriteLock有两个核心方法:lock和unlock。它们都会间接调用了ReentrantReadWriteLock内部同步器的对应方法,在同步器中需要重写tryAcquire方法和tryRelease方法,分别用于获取写锁和释放写锁操作。

先看tryAcquire方法的逻辑,获取状态值并通过exclusiveCount方法得到低16位的写锁状态值。c!=0时有两种情况,一种是高16位的读锁状态不为0,一种是低16位的写锁状态不为0。w等于0时表示还有线程持有读锁,直接返回false表示获取写锁失败。如果持有写锁的线程为当前线程,则表示写锁重入操作,此时需要将状态变量进行累加,此外需要校验的是写锁重入状态值不能超过MAX_COUNT。通过writerShouldBlock方法判断是否需要将当前线程放入排队队列中,同时通过拥有CAS算法的compareAndSetState方法对状态变量进行累加操作,CAS失败的话也需要将当前线程放入排队队列中。对于非公平模式,这里的CAS操作就是闯入操作,即线程先尝试一次竞争写锁。最后通过setExclusiveOwnerThread设置当前线程持有写锁,该方法只是简单的设置变量方法。

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数前端工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Web前端开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
img
img
img
img
img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上前端开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新

如果你觉得这些内容对你有帮助,可以添加V获取:vip1024c (备注前端)
img

最后:

总结来说,面试成功=基础知识+项目经验+表达技巧+运气。我们无法控制运气,但是我们可以在别的地方花更多时间,每个环节都提前做好准备。

面试一方面是为了找到工作,升职加薪,另一方面也是对于自我能力的考察。能够面试成功不仅仅是来自面试前的临时抱佛脚,更重要的是在平时学习和工作中不断积累和坚持,把每个知识点、每一次项目开发、每次遇到的难点知识,做好积累,实践和总结。

CodeChina开源项目:【大厂前端面试题解析+核心总结学习笔记+真实项目实战+最新讲解视频】

一个人可以走的很快,但一群人才能走的更远。如果你从事以下工作或对以下感兴趣,欢迎戳这里加入程序员的圈子,让我们一起学习成长!

AI人工智能、Android移动开发、AIGC大模型、C C#、Go语言、Java、Linux运维、云计算、MySQL、PMP、网络安全、Python爬虫、UE5、UI设计、Unity3D、Web前端开发、产品经理、车载开发、大数据、鸿蒙、计算机网络、嵌入式物联网、软件测试、数据结构与算法、音视频开发、Flutter、IOS开发、PHP开发、.NET、安卓逆向、云计算

、MySQL、PMP、网络安全、Python爬虫、UE5、UI设计、Unity3D、Web前端开发、产品经理、车载开发、大数据、鸿蒙、计算机网络、嵌入式物联网、软件测试、数据结构与算法、音视频开发、Flutter、IOS开发、PHP开发、.NET、安卓逆向、云计算**

  • 29
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值