之前分析过synchronized和ReentrantLock,这两种锁都是排他锁,即一个资源同一时刻只能被一个线程占有,线程释放了锁之后,其他线程才能去抢占这个资源。但是JUC中还有一个读写锁ReentrantReadWriteLock,可以提升读远远多于写这种场景下的性能,本文当算来分析下它的内部原理。
读写锁概念介绍:
获取锁的方式和ReentrantLock很像,都是基于AQS实现的,但是由于读写锁内部维护了读和写两种状态,实现上要比ReentrantLock复杂。读写锁主要有如下特性:
- 和ReentrantLock一样有公平和非公平两种模式,默认情况下使用的是后者,原因是吞吐量较优。
- 支持可重入
- 锁降级:如果长时间用写锁独占,显然对于某些高响应的应用是不允许的,所以在完成部分写操作后,退而使用读锁降级,来允许响应其他进程的读操作。只有当全部事务完成后才真正释放锁。(但是读锁不能升级成写锁,这种情况下回引发思索,后面会讲到)
读写锁源码分析:
构造函数和成员变量:
状态标识:
ReentrantLock使用AQS中的state变量来表示重入次数,但是读写锁里面有两种锁,它怎么表示呢?
从图片中可以看出,ReentrantReadWriteLock将state这个int型变量分为高16位和低16位,高16位表示当前读锁的占有量,低16位表示写锁的占有量。(这里的占有量是指,入一个线程加1,这个线程重入一次也加一???)
读锁还用变量记录了其他读锁相关的信息,这几个属性是提升性能用的:
//第一个获取到读锁的线程
private transient Thread firstReader = null;
//第一个线程的重入数量
private transient int firstReaderHoldCount;
//最后一个线程的重入数量
private transient HoldCounter cachedHoldCounter;
//每个线程维护自己的重入数量
private transient ThreadLocalHoldCounter readHolds;
但是从本人分析看来,前面三个好像也没啥太大的作用...可能在读锁不产生竞争的情况下,直接使用firstReader不用到ThreadLocal里面去找了可以加快一点效率...也可能是我理解起来的姿势不对
写锁获取过程:
和ReentrantLock一样,调用的是AQS同步器中的acquire(1)方法,然后重写了其中的tryAcquire()逻辑。
看下writeShouldBlock():
//非公平模式下,一直返回的是false。也就是说当前来获取写锁的这个线程的优先级最大,可以直接抢占写锁
//公平模式下一直都是排队的
写锁释放过程:
和ReentrantLock一样,调用的是AQS同步器中的release(1)方法,然后重写了其中的tryRelease()逻辑。
读锁获取过程:
调用的是AQS中的sync.acquireShared(1),然后重写了tryAcquireShared()方法。
看下readerShouldBlock():
//公平模式下一直都是排队的
//非公平模式下,要避免写锁一直等待
读锁释放过程:
调用的是AQS中的sync.releaseShared(1),然后重写了tryReleaseShared()方法。
总结:读写锁获取流程图
//读锁获取流程
//写锁获取流程
总之原则就是:
写写互斥
读读不互斥
同一个线程,写锁可以降级为读锁
公平模式下大家都排队;非公平模式下,写锁可以直接抢占,读锁需要保证队列中的写锁不能死等
注意:慎用读写锁
读写锁源码中有这么一段注释:
* 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.
意思就是说,并不是所有读大于写的场景都适合使用读写锁。还要满足操作的开销要大于同步的开销,个人理解下就是加锁和释放锁这段时间过程中的操作耗时要大于加锁这个动作的耗时,读写锁使用才有意义,比如操作一个大集合。
为什么还要加上获取锁的时间比较长这个限制呢?个人理解是读写锁获取锁时候的开销和普通的排他锁性能是差不多的,可能还要差一点。如果获取锁的时间不长,还是会和普通的排他锁一样出现频繁上下文切换的问题,而如果获取锁的时间长,多个读线程可以一起读,这样其实就提高了并发读的效率!
参考:
http://blog.guoyb.com/2018/02/11/rwlock/(读写锁的性能一定好吗)
https://blog.csdn.net/ysu108/article/details/39343295(慎用读写锁)
https://blog.csdn.net/a724888/article/details/60879174(firstHeadCount变量的作用)