前言
上午看了AQS和ReentryLock的源码,下午趁热打铁把ReentryReadWriteLock的源码也看了,其实只要理解了AQS大概流程,基于AQS的锁都很好理解。下面的总结是在熟悉了AQS结构的基础上进行的。
读写锁
在看源码前,如果想要基于AQS或者说类似monitor的结构实现一个读写锁应该是什么思路?回忆一下上一篇AQS笔记提到的AQS结构中常用的四个变量,state,head,tail,ownerThread
- 读写锁能做什么?
- 读读可以并发
- 读的时候不能写
- 写的时候不能读
- 写写不能并发
- 读读并发怎么实现?
- 原本state只能当1种锁的占位符,现在有读写两种锁,那么考虑state拆成state_read和state_write分别表示读写锁的占位符
- 那么读读并发只需要在对state_read占锁的时候判断state_write是否被占锁即可,不需要管state_read有没有锁,因此做法就是如果state_write==0,则对state_read进行累加
- 释放的时候就把state_read变成0
- 读的时候不能写怎么实现?
- 线程抢占写锁的时候判断 if(state_read==0),为0才能占锁即可
- 写的时候不能读怎么实现?
- 线程占读锁的时候判断 if(state_write==0), 为0才能占读锁
- 写写不能并发怎么实现?
- state_write不为0则不能占写锁
以上思路其实可以把ReentryLock变成一个简单的读写锁了,但还不够,需要考虑如下问题
- 读读并发怎么实现可重入?
- 读锁的可重入读可以当作并发读,因此原来的读读并发实现可以完成读锁的可重入
- 写写并发怎么实现可重入?
- 占写锁的时候,把当前线程设为ownerThread
- 写锁的可重入写发生时,判断state_write不为1后再判断ownerThread是否为当前线程即可,如果是,则允许获得锁
PS: 从上述两个实现方案对比发现,读读并发的实现并不需要原来的ReentryLock的ownerThread结构了, 为此后面看源码会发现AQS提供了另一个入口规范称为acquireShared(), 而原来的入口称为acquire()。顾名思义,acquireShared是为了shared的,根据上面的分析,共享读时是不需要考虑ownerThread的,故读锁入口是acquireShared,而写锁还得考虑ownerThread,因此得用acquire,acquire()在源码备注中说明是独占的入口。
占锁思路貌似解决了,那么就到阻塞队列应该怎么设计了
- 其实还是和单锁一样,拿不到锁放入阻塞队列排队就行了
- 当某个线程抢到写锁后,还有线程需要抢锁,此时不管是读锁请求还是写锁请求都需要排队,那么队列中可能就是 读-》读-》写-》读-》写-》写-》读
- 其实排队都一样,就是抢不到就排就完事了
- 不同的在于怎么唤醒队列的线程
- 上述队列情况主要包括几种
- 读读
- 假设写线程释放了锁,唤醒队列中第一个读线程
- 读线程被唤醒后,由于读读并发,应该让其试图唤醒其后面的读锁
- 以此类推,也就是当第一个读被唤醒时,其后面连续的读都应该被唤醒
- 最终连续的读都会被唤醒,都可以开始进行读读并发
- 这里补充一个细节。如何唤醒队列中连续的读线程?
- 当写线程释放锁后,调用unpark方法把连续的读线程全部唤醒?这样肯定不行,这就意味着队列里的连续读线程同时被唤醒,同时抢锁,失去排队意义
- 做法应该是写线程释放锁后只唤醒队列中第一个读线程
- 读线程在获取到锁之后,尝试唤醒其后线程
- 对应AQS中的doAcquireShared中的setHeadAndPropagate,其中会执行doReleaseShared。注意这是走acquiredShared入口会进入的方法
- 如果走的独占入口acquired,在获取到锁后是不需要再唤醒后面的线程的,这就是区别
- 正因为上述两种执行逻辑不同,AQS才设计了共享入口acquiredShared和独占入口acquired
- 读写
- 第一个读被唤醒后,试图唤醒后面一个写,该写试图抢占写锁,但会发现state_read不为0,因此继续阻塞
- 写读
- 类似读写
- 写写
- 写写就和ReentryLock一样的流程了
- 读读
- 解决完上述问题,一个简单的读写锁思路应该差不多了
上述思路是没看读写锁源码的时候能够想到的,去看源码发现其实就是这个思路。因此就不再解析源码的流程了。总结几个我认为比较有意思的点
- 读写锁的实现上,其实就是在ReentryLock的基础上,加入了共享读的逻辑,为了更好的加入该逻辑,AQS除了acquire入口外,还定义了acquireShared入口,从这个入口进去,实现的是共享逻辑,对应的队列中获取锁的方式也是共享的。那么阅读ReadLock的源码会发现,其lock用的就是该入口。
- state变量并不像我上述思路一样用两个变量来对应两个锁的占位符,而是将两个变量合成一个,用一个变量的高低位分别表示两个锁的占位符,目的是保证CAS的有效性
- 其他的类似队列的数据结构是链表,以及head,tail为volatile等保证线程安全的细节就不赘述,都在上一篇AQS总结中提到
- 感觉以上分析基本能把读写锁原理搞清楚了
在最后,看源码时突然想到一个AQS的问题,仅供参考
Q: 非公平锁在新一轮竞争锁时,若原阻塞队列中的线程与新来的线程抢锁失败,原阻塞队列中的线程何去何从?
- 原阻塞队列的线程还在阻塞队列头部
- 因为当它被唤醒去抢锁时,只有抢到锁了才会被拿出阻塞队列,否则继续阻塞