丈夫志四海,万里犹比邻。这篇文章主要讲述33张图解析ReentrantReadWriteLock源码相关的知识,希望能为你提供帮助。
大家好,我是阿星,今天是一篇硬核文,请各位读者大大们系好安全带,马上要发车了。
文章图片
晕车的朋友,可以先吃一颗阿星独家秘制的晕车药,童叟无欺,货真价实,还免费,白嫖党狂喜(16张图揭开AQS)。
本文大纲如下
文章图片
纵观全局我的英文名叫
ReentrantReadWriteLock
(后面简称RRW
),大家喜欢叫我读写锁,因为我常年混迹在读多写少的场景。读写锁规范作为合格的读写锁,先要有读锁与写锁才行。
所以声明了
ReadWriteLock
接口,作为读写锁的基本规范。文章图片
之后都是围绕着规范去实现读锁与写锁。
读锁与写锁WriteLock与ReadLock就是读锁和写锁,它们是
RRW
实现ReadWriteLock
接口的产物。但读锁、写锁也要遵守锁操作的基本规范.
所以WriteLock与ReadLock都实现了
Lock
接口。文章图片
那么WriteLock与ReadLock对Lock接口具体是如何实现的呢?
自然是少不了我们的老朋友
AQS
了。AQS众所周知,要实现锁的基本操作,必须要仰仗
AQS
老大哥了。AQS(AbstractQueuedSynchronizer)
抽象类定义了一套多线程访问共享资源的同步模板,解决了实现同步器时涉及的大量细节问题,能够极大地减少实现工作,用大白话来说,AQS
为加锁和解锁过程提供了统一的模板函数,只有少量细节由子类自己决定。AQS简化流程图如下
文章图片
SyncAQS为加锁和解锁过程提供了统一的模板函数,只有少量细节由子类自己决定,但是WriteLock与ReadLock没有直接去继承AQS。
因为WriteLock与ReadLock觉得,自己还要去继承
AQS
实现一些两者可以公用的抽象函数,不仅麻烦,还有重复劳动。所以干脆单独提供一个对锁操作的类,由WriteLock与ReadLock持有使用,这个类叫
Sync
。Sync继承AQS实现了如下的核心抽象函数
- tryAcquire
- release
- tryAcquireShared
- tryReleaseShared
文章图片
其中tryAcquire、release是为
WriteLock
写锁准备的。tryAcquireShared、tryReleaseShared是为
ReadLock
读锁准备的,这里阿星后面会说。上面说了Sync实现了一些
AQS
的核心抽象函数,但是Sync本身也有一些重要的内容,看看下面这段代码文章图片
我们都知道
AQS
中维护了一个state
状态变量,正常来说,维护读锁与写锁状态需要两个变量,但是为了节约资源,使用高低位切割实现state
状态变量维护两种状态,即高16
位表示读状态,低16
位表示写状态。Sync中还定义了HoldCounter与ThreadLocalHoldCounter
- HoldCounter是用来记录读锁重入数的对象
- ThreadLocalHoldCounter是ThreadLocal变量,用来存放第一个获取读锁线程外的其他线程的读锁重入数对象
文章图片
公平与非公平策略你看,人家
ReentrantLock
都有公平与非公平策略,所以ReentrantReadWriteLock
也要有。什么是公平与非公平策略?
因为在
AQS
流程中,获取锁失败的线程,会被构建成节点入队到CLH
队列,其他线程释放锁会唤醒CLH
队列的线程重新竞争锁,如下图所示(简化流程)。文章图片
非公平策略是指,非
CLH
队列的线程与CLH
队列的线程竞争锁,大家各凭本事,不会因为你是CLH
队列的线程,排了很久的队,就把锁让给你。公平策略是指,严格按照
CLH
队列顺序获取锁,一定会让CLH
队列线程竞争成功,如果非CLH
队列线程一直占用时间片,那就一直失败,直到时间片轮到CLH
队列线程为止,所以公平策略的性能会更差。文章图片
回到正题,为了支持公平与非公平策略,Sync扩展了
FairSync、NonfairSync
子类,两个子类实现了readerShouldBlock、writerShouldBlock函数,即读锁与写锁是否阻塞。文章图片
关于readerShouldBlock、writerShouldBlock函数在什么地方使用阿星后面会说。
ReentrantReadWriteLock全局图最后阿星把前面讲过的内容,全部组装起来,构成下面这张图。
文章图片
有了全局观后,后面就可以深入细节逐个击破了。
深入细节后面我们只要攻破
5
个细节就够了,分别是读写锁的创建、获取写锁、释放写锁、获取读锁、释放读锁。ReentrantReadWriteLock的创建读写锁的创建,会初始化化一系列类,代码如下
文章图片
ReentrantReadWriteLock
默认是非公平策略,如果想用公平策略,可以直接调用有参构造器,传入true
即可。但不管是创建FairSync还是NonfairSync,都会触发
Sync
的无参构造器,因为Sync
是它们的父类(本质上它们俩都是Sync)。文章图片
因为Sync需要提供给ReadLock与WriteLock使用,所以创建ReadLock与WriteLock时,会接收
ReentrantReadWriteLock
对象作为入参。文章图片
最后通过
ReentrantReadWriteLock.sync
把Sync
交给了ReadLock与WriteLock。获取写锁我们遵守ReadWriteLock接口规范,调用
ReentrantReadWriteLock.writeLock
函数获取写锁对象。文章图片
获取到写锁对象后,遵守Lock接口规范,调用
lock
函数获取写锁。WriteLock.lock函数是由
Sync
实现的(FairSync或NonfairSync)。文章图片
sync.acquire(1)
函数是AQS中的独占式获取锁流程模板(Sync继承自AQS)。文章图片
WriteLock.lock调用链如下图
文章图片
我们只关注
tryAcquire
函数,其他函数是AQS的获取独占式锁失败后的流程内容,不属于本文范畴,tryAcquire
函数代码如下文章图片
为了易于理解,阿星把它转成流程图
文章图片
通过流程图,我们发现了一些要点
- 读写互斥
- 写写互斥
- 写锁支持同一个线程重入
- writerShouldBlock写锁是否阻塞实现取决公平与非公平的策略(FairSync和NonfairSync)
unlock
函数释放写锁(Lock接口规范)。WriteLock.unlock函数也是由Sync实现的(FairSync或NonfairSync)。
文章图片
sync.release(1)
执行的是AQS中的独占式释放锁流程模板(Sync继承自AQS)。文章图片
WriteLock.unlock调用链如下图
文章图片
再来看看
tryRelease
函数,其他函数是AQS的释放独占式成功后的流程内容,不属于本文范畴,tryRelease
函数代码如下文章图片
为了易于理解,阿星把它转成流程图
文章图片
因为同一个线程可以对相同的写锁重入多次,所以也要释放的相同的次数。
获取读锁我们遵守ReadWriteLock接口规范,调用
ReentrantReadWriteLock.readLock
函数获取读锁对象。文章图片
获取到读锁对象后,遵守Lock接口规范,调用
lock
函数获取读锁。ReadLock.lock函数是由
Sync
实现的(FairSync或NonfairSync)。文章图片
sync.acquireShared(1)
函数执行的是AQS中的共享式获取锁流程模板(Sync继承自AQS)。文章图片
ReadLock.lock调用链如下图
文章图片
我们只关注
tryAcquireShared
函数,doAcquireShared函数是AQS的获取共享式锁失败后的流程内容,不属于本文范畴,tryAcquireShared
函数代码如下文章图片
代码还挺多的,为了易于理解,阿星把它转成流程图
文章图片
通过流程图,我们发现了一些要点
- 读锁共享,读读不互斥
- 读锁可重入,每个获取读锁的线程都会记录对应的重入数
- 读写互斥,锁降级场景除外
- 支持锁降级,持有写锁的线程,可以获取读锁,但是后续要记得把读锁和写锁读释放
- readerShouldBlock读锁是否阻塞实现取决公平与非公平的策略(FairSync和NonfairSync)
unlock
函数释放读锁(Lock接口规范)。ReadLock.unlock函数也是由Sync实现的(FairSync或NonfairSync)。
文章图片
sync.releaseShared(1)
函数执行的是AQS中的共享式释放锁流程模板(Sync继承自AQS)。文章图片
ReadLock.unlock调用链如下图
文章图片
我们只关注
tryReleaseShared
函数,doReleaseShared函数是AQS的释放共享式锁成功后的流程内容,不属于本文范畴,tryReleaseShared
函数代码如下文章图片
为了易于理解,阿星把它转成流程图
文章图片
这里有三点需要注意
- 第一点:线程读锁的重入数与读锁数量是两个概念,线程读锁的重入数是每个线程获取同一个读锁的次数,读锁数量则是所有线程的读锁重入数总和。
- 第二点:AQS的共享式释放锁流程模板中,只有全部的读锁被释放了,才会去执行doReleaseShared函数
- 第三点:因为使用的是AQS共享式流程模板,如果CLH队列后面的线程节点都是因写锁阻塞的读锁线程节点,会传播唤醒
ReentrantReadWriteLock
底层实现与ReentrantLock
思路一致,它们都离不开AQS
,都是声明一个继承AQS
的Sync
,并在Sync
下扩展公平与非公平策略,后续的锁相关操作都委托给公平与非公平策略执行。我们还发现,在
AQS
中除了独占式模板,还有共享式模板,它们在多线程访问共享资源的流程会有所差异,就如ReentrantReadWriteLock
中读锁使用共享式,写锁使用独占式。最后再捋一捋写锁与读锁的逻辑
- 读读不阻塞
- 写锁阻塞写之后的读写锁,但是不阻塞写锁之前的读锁线程
- 写锁会被写之前的读写锁阻塞
- 读锁节点唤醒会无条件传播唤醒CLH队列后面的读锁节点
- 写锁可以降级为读锁,防止更新丢失
- 读锁、写锁都支持重入
- 点击获取《零基础到Java就业全套教程、架构师全套教程》
- 透彻Java线程状态转换
- 图文并茂的聊聊ReentrantReadWriteLock的位运算
- 通俗易懂的ReentrantLock,不懂你来砍我
- 万字长文 | 16张图解开AbstractQueuedSynchronizer
- 写给小白看的LockSupport
- 13张图,深入理解Synchronized
- 由浅入深CAS,小白也能与BAT面试官对线
- 小白也能看懂的Java内存模型
- 保姆级教学,22张图揭开ThreadLocal
- 进程、线程与协程傻傻分不清?一文带你吃透!
- 什么是线程安全?一文带你深入理解
java
程序猿,公众号「程序猿阿星」 定期分享有趣有料的精品原创文章!非常感谢各位小哥哥小姐姐们能看到这里,原创不易,文章有帮助可以关注、点个赞、分享与评论,都是支持(莫要白嫖)!
愿你我都能奔赴在各自想去的路上,我们下篇文章见。
【33张图解析ReentrantReadWriteLock源码】
文章图片
推荐阅读
- 如何实现 Android 短视频跨页面的流畅续播()
- 数据结构——时间复杂度空间复杂度
- 学习Linux运维的个人笔记(LinuxBash的特性)
- 学习Linux运维的个人笔记(Linux目录结构及文件路径定位)
- 学习Linux运维的个人笔记(Vmware的简单应用及LinuxBash介绍)
- 学习Linux运维的个人笔记(安装Centos7)
- nginx
- 学习Linux运维的个人笔记(Linux基本介绍)
- Linux系统中查找文件