006-ReentrantReadWriteLock和StampLock详解

ReentrantReadWriteLock

场景

我们直到独占锁每次访问资源只允许一个线程
但是我们程序往往是读多写少
只允许一个线程的话性能会由很大限制
所以就有了读写锁
我在读的时候,另外一个读的操作也可以获取锁
我在读的时候,另外一个写的操作不能获取锁,反之我在写的时候,读线程也不能获取锁
我在写的时候,另外一个写的线程不能获取锁

  • 读读共享
  • 读写互斥
  • 写写互斥
    ReentrantReadWriteLock就是这样一把可重入读写锁

示例:
在这里插入图片描述
在这里插入图片描述
模拟读读场景
在这里插入图片描述
在这里插入图片描述
模拟读写场景
在这里插入图片描述
写线程再读线程结束后才能拿到锁进行操作
在这里插入图片描述
如果写线程放第一个申请锁,那就只有写完释放锁了读操作才能进行
在这里插入图片描述
模拟写写场景
在这里插入图片描述

分析

目前用下来可能由如下几个问题

  1. AQS只有一个状态,怎么能发两把锁,也就是读写互斥怎么实现?
  2. 读锁的可重入是怎么实现的?

可以先说一下结论,后边分析代码验证

一、读写互斥是根据 state 字段的高低位来判断的
int类型有4个字节,也就是4*8=32位
高16位给读锁用
低16位给写锁用
在判断的时候只需要进行移位操作后判断是否 >0 即可
二、读锁的可重入是使用ThreadLocal实现的
我们直到独占锁的可重入是判断是否是独占线程后state+1这个实在实现类中tryLock实现的
所以是否可重入完全就可以由实现类来控制

源码验证分析

我们先看写锁
因为写锁肯定是独占锁,逻辑比较熟悉
在这里插入图片描述
具体逻辑在tryAcquire(1)中
在这里插入图片描述
代码大致分了3块

  1. 获取锁标志state
  2. 有其他锁操作
  3. 没有锁操作

我们分别来看

写锁-tryAcquire()获取锁标志

exclusiveCount(state)
在这里插入图片描述
这个涉及到位运算,但是这个位运算很简单 SHARED_SHIFT = 16
2 16 − 1 2^{16}-1 2161
数学上:获取16位二进制的最大值
物理上:获取16位都是1的二进制数
这是一个范式很多地方会用,我们记住就行
而 c & EXCLUSIVE_MASK (11得1,有0得0)
计算下来 c的高位全部抹零了,低位全部保留了
所以写锁就是用 state 的低16位做锁的标记

写锁-tryAcquire()没有其他锁分支

writerShouldBlock()
这个直接返回了false:在重入判断的时候设置不需要阻塞,也就是可重入用的
然后就 CAS state 从原值到原值+1,
这里也是重入的逻辑
然后返回true取反为false
设置独占锁线程,返回true

写锁-tryAcquire()有其他锁

在这里插入图片描述
因为这个是所锁分支
所以这里判断没有写锁或者不是当前线程独占,就返回false
在这里插入图片描述
这里是判断一下因为只有16位长度了加多了就加到读锁那里了,所以就直接异常了
如果校验通过,那就是重入了
直接设置state,返回true

写锁的加锁逻辑ok了
下边看下读锁

读锁-加锁

因为是一把共享锁
在这里插入图片描述
所以实现的是 tryAcquireShard(1)
我们在AQS那边其实已经分析过了
规范是如果tryAcquireShard(1)<0就是加锁失败
那我们就进入tryAcquireShared(1)看一下如何尝试加锁
在这里插入图片描述
加锁失败场景
exclusiveCount(1)就是获取写锁标记,非0则存在写锁
且当前独占线程不是当前线程则直接加锁失败

加锁成功场景-第一个线程
判断是否应该Block*

这个Block里边有解释,大致说的就是如果一个写线程在申请了那后续的读线程都先block,避免写线程很长时间或者长时间无法获取锁

不需要Block 并且 写锁小于 2^16-1 并且 CAS state 从 0 到 高16位 + 1成功
然后:
判断原读锁为0,那就是第一个读线程进入了
设置 firstReader 和 firstReaderHoldCountfirstReader = 1

判断原读锁不为0,那就是2种可能

  1. 第一个读线程重入
  2. 后续其他读线程进入了

第一个读线程重入,firstReaderHoldCountfirstReader ++
其他读线程进入了
如果当前线程HoldCounter为空或者当前缓存的HoldCounter属于当前线程:那就去创建一个ThreadLocal<HoldCounter>
如果已经有HoldCounter但是值为0 则把HoldCounter塞入当前线程
然后就是HoldCounter种的count++

总结来说
如果时读锁的第一个线程,则重入次数维护在lock对象中的firstReaderHoldCount字段中
如果非第一个线程则维护在ThreadLocal中

最后的return fullTryAcquireShared(current);
则是CAS失败或者进入Block
只要来说就是设置一个自旋,并且每种失败场景分开处理
CAS失败、block则自选
超限则抛出异常

ReentrantReadWriteLock问题

同线程中,写锁可以降级位读锁,但是读锁不能升级为写锁

注意:写锁降级为读锁,必须加读锁在写锁内,避免出现脏读问题,加读锁也可以保证可见性

StampLock

比读写锁更快的锁
对比读写锁提升就是因为使用了乐观锁
因为不支持可重入,所以使用有一些限制
重点分析下示例用法
在这里插入图片描述
在申请读锁的时候有2个特殊的方法
sl.tryOptimisticRead()
sl.validate(stamp)

tryOptimisticRead,向乐观读锁申请一个版本标记
获取变量
然后validate判断一下标记是否被修改
如果已经无效也就是别写锁修改
那么就申请悲观读锁(读写互斥)
重新读一下

tryConvertToWriteLock
只有线程在有读锁的情况下申请尝试升级写锁,如果成功则返回非0
如果失败表示有其他写锁线程,则先释放读锁并获取写锁才能进行写操作
这样的好处就是如果没有其他写锁线程,那就直接可以更新成功而不需要获取写锁

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值