33 张图解析 ReentrantReadWriteLock 源码
大家好,我是阿星,今天是一篇硬核文,请各位读者大大们系好安全带,马上要发车了。
晕车的朋友,可以先吃一颗阿星独家秘制的晕车药,童叟无欺,货真价实,还免费,白嫖党狂喜(16张图揭开AQS)。
本文大纲如下
纵观全局
我的英文名叫ReentrantReadWriteLock
(后面简称RRW
),大家喜欢叫我读写锁,因为我常年混迹在读多写少的场景。
读写锁规范
作为合格的读写锁,先要有读锁与写锁才行。
所以声明了ReadWriteLock
接口,作为读写锁的基本规范。
之后都是围绕着规范去实现读锁与写锁。
读锁与写锁
WriteLock 与 ReadLock 就是读锁和写锁,它们是RRW
实现ReadWriteLock
接口的产物。
但读锁、写锁也要遵守锁操作的基本规范.
所以 WriteLock 与 ReadLock 都实现了Lock
接口。
那么 WriteLock 与 ReadLock 对 Lock 接口具体是如何实现的呢?
自然是少不了我们的老朋友AQS
了。
AQS
众所周知,要实现锁的基本操作,必须要仰仗AQS
老大哥了。
AQS(AbstractQueuedSynchronizer)
抽象类定义了一套多线程访问共享资源的同步模板,解决了实现同步器时涉及的大量细节问题,能够极大地减少实现工作,用大白话来说,AQS
为加锁和解锁过程提供了统一的模板函数,只有少量细节由子类自己决定。
AQS 简化流程图如下
如果读者想深入 AQS 细节,可以看阿星的这篇文章:16张图揭开AQS
Sync
AQS 为加锁和解锁过程提供了统一的模板函数,只有少量细节由子类自己决定,但是 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
位表示写状态。
关于读写锁状态设计具体细节可以看阿星的文章:ReentrantReadWriteLock的位运算
Sync 中还定义了 HoldCounter 与 ThreadLocalHoldCounter
HoldCounter 是用来记录读锁重入数的对象
ThreadLocalHoldCounter 是 ThreadLocal 变量,用来存放第一个获取读锁线程外的其他线程的读锁重入数对象
如果读者对
ThreadLocal
不太熟悉,可以去看阿星的文章: 保姆级教学,22张图揭开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
程序猿,公众号 「程序猿阿星」 定期分享有趣有料的精品原创文章!
非常感谢各位小哥哥小姐姐们能看到这里,原创不易,文章有帮助可以关注、点个赞、分享与评论,都是支持(莫要白嫖)!
愿你我都能奔赴在各自想去的路上,我们下篇文章见。
版权声明: 本文为 InfoQ 作者【程序猿阿星】的原创文章。
原文链接:【http://xie.infoq.cn/article/349049634e045b8b677d9c6b6】。文章转载请联系作者。
评论