java多线程之读写锁

前言

上午看了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: 非公平锁在新一轮竞争锁时,若原阻塞队列中的线程与新来的线程抢锁失败,原阻塞队列中的线程何去何从?

  • 原阻塞队列的线程还在阻塞队列头部
  • 因为当它被唤醒去抢锁时,只有抢到锁了才会被拿出阻塞队列,否则继续阻塞
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值