概要
ReentrantReadWriteLock顾名思义”可重入的读写锁”,它跟ReentrantLock有点类似。ReentrantReadWriteLock主要是利用读写分离的思想,读取数据使用“读锁” 实现多个读线程可以并行读;写数据使用“写锁” 实现只能由一个线程写入。相对于ReentrantLock,在读多写少的情况下,使用ReentrantReadWriteLock会有更好的性能表现。
本文将介绍ReentrantReadWriteLock的使用及实现原理。
应用场景
ReentrantReadWriteLock的使用场景就是在读多写少情况下以提高性能。看看官方文档的描述以及例子:
ReentrantReadWriteLocks can be used to improve concurrency in some
uses of some kinds of Collections. This is typically worthwhile
only when the collections are expected to be large, accessed by
more reader threads than writer threads, and entail operations with
overhead that outweighs synchronization overhead. For example, here
is a class using a TreeMap that is expected to be large and concurrently accessed.
对TreeMap加读写锁例子:
1 | class RWDictionary { |
实现原理
对ReentrantReadWriteLock有了一个感性的认识后,我们看看它的实现原理。
首先ReentrantReadWriteLock是基于AQS
实现的,如果你对AQS还不是很了解,建议你先看看AQS。
读写/内部状态
在了解到ReentrantReadWriteLock后,你是否有个疑问,它是怎么区分读锁和写锁的?
像ReentrantLock一样,ReentrantReadWriteLock内部有一个Sync
类,它继承AbstractQueuedSynchronizer
,我们知道AQS内部只有一个int类型的state
变量来表示状态。那么ReentrantReadWriteLock.Sync是怎么做的呢?
1 | /* |
用state 的高 16 位代表读锁的获取次数;用state 的低 16 位代表写锁的获取次数
如图所示:(网络盗图)
分别用16位来保存读和写的状态,即同时读的线程数和同时写的线程数(重入的次数,因为写锁是独占模式)。通过位运算来获得高16位或低16位的数。
在看看读锁和写锁在ReentrantReadWriteLock内部的表示方式
1 | /** Inner class providing readlock */ |
可以看出内部分别持有一个ReadLock
和WriteLock
对象。从api得知上锁和解锁的操作都是由这两类完成的,下面将详细分析这两个类的作用。
写锁WriteLock
WriteLock
是ReentrantReadWriteLock的内部类,它持有一个Sync
的实例。如下代码:
1 | private final Sync sync; |
看看有哪些方法:
跟ReentrantLock长得很像,用法都类似,有lock,tryLock,支持中断方式的,支持超时的,释放锁等。
由于Sync
继承AbstractQueuedSynchronizer
,所以Sync的用法其实都是在AQS这个框架里的,在下面源码分析中,不会过多解释AQS部分的代码,希望读者提前了解AQS。
着重分析lock
、tryLock
、unlock
方法,其他方法也是差不多的。
lock
写锁说明:
- 写锁是独占锁。
- 如果有读锁被占用,写锁获取是要进入到阻塞队列中等待的,即读写互斥。
1 | public void lock() { |
调用Sync的acquire
,这是AQS的方法,最终还是会调用tryAcquire
这个方法
1 | protected final boolean tryAcquire(int acquires) { |
tryLock
tryLock
直接调用Sync
的tryWriteLock
方法,它跟lock
不同,它不会阻塞,获取锁失败直接返回false,所以这个方法是不经过AQS的。
1 | public boolean tryLock( ) { |
1 | /** |
unlock
1 | public void unlock() { |
1 | /* |
读锁ReadLock
ReadLock
是ReentrantReadWriteLock的内部类,它持有一个Sync
的实例。如下代码:
1 | private final Sync sync; |
看看有哪些方法:
跟WriteLock
长得很像,也是重分析lock
、tryLock
、unlock
方法。
读锁线程缓存
ReentrantReadWriteLock对读锁的线程及读锁次数有缓存,来增加性能,因为在下文会提到,所以先了解一下比较好
1 | /** |
lock
读锁的获取,由于读锁是共享的,所以用到了AQS共享模式那套api。
一样的套路,lock
方法调用的是AQS的acquireShared
方法,而最终会先调用Sync
的tryAcquireShared
方法
1 | public void lock() { |
1 | protected final int tryAcquireShared(int unused) { |
总结一下:tryAcquireShared
整体代码算有点多,也算有点复杂,不过多看几次就会发现代码有一定的重复性,看代码时要想清楚有哪些情景,在这里你会发现情景无非就几种,加上对读锁线程的缓存和threadlocal的处理可能会对你来说增加了一定的复杂度,不过多看几次就好了。
tryLock
tryLock
不过AQS,整体处理跟tryAcquireShared
类似,而且更加简单,因为不用考虑CLH队列的情况。
1 | public boolean tryLock() { |
1 | /** |
在熟悉tryAcquireShared
后,你会发现tryReadLock
不难,而且处理流程几乎一致。
unlock
AQS的套路unlock
-> releaseShared
-> tryReleaseShared
。
1 | public void unlock() { |
特性
ReentrantReadWriteLock支持一些特性
- 支持公平/非公平
- 锁降级
- 锁重入
- 可中断
- 支持Condition条件
我们着重看看前面两个特性,后面三个是跟ReentrantLock一样的。
公平/非公平
公平策略在ReentrantLock也有,不过在ReentrantReadWriteLock中对于读锁和写锁的处理是有点不同的的。
其实在上面加锁过程的分析中也提到了对于公平和非公平的处理,下面再详细看看。
公平锁由FairSync
实现,继承Sync
;非公平锁由NonfairSync
实现,继承Sync
。
它们两者都是重写了writerShouldBlock
和readerShouldBlock
方法,对于不同策略下的读写是否应该阻塞的判断。
1 | /** |
锁降级
锁降级上面也提到过,意思是在自身获得写锁的情况下,可以无条件获得读锁,反之则不行。
看看官方的描述
Lock downgrading
Reentrancy also allows downgrading from the write lock to a read lock,
by acquiring the write lock, then the read lock and then releasing the
write lock. However, upgrading from a read lock to the write lock is not possible.
这个缓存数据读写的官方demo,对于锁降级还是读写锁的使用都有参考的价值
1 | class CachedData { |
总结
ReentrantReadWriteLock在读多写少的场景下,有更高的性能;
ReentrantReadWriteLock是基于AbstractQueuedSynchronizer实现的,利用AQS的state状态的高16位表示读状态,低16位表示写状态,达到读写互斥的效果;支持公平/非公平的策略,锁降级等特性。
最后再补充一下,ReentrantReadWriteLock的写表示独占锁,读表示共享锁,所以ReentrantReadWriteLock是用到了AQS的两种模式,在CLH阻塞队列中有可能同时存在共享节点和独占节点。
如图所示:
参考资料
https://juejin.im/post/5b7a834551882542c20f1985
https://javadoop.com/post/reentrant-read-write-lock