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】。文章转载请联系作者。











评论