JDK源码系列——ReentrantReadWriteLock源码解析



前言

ReentrantReadWriteLock是JDK提供的读写锁,读写锁是把对共享资源的访问分为读访问和写访问,读读是不互斥的,读写是互斥的,使用读写锁可以极大提高并发量,适用于读多写少的场景。


1、重要属性解析

主要维护了读锁,写锁和同步器,这里可以看到它有四个内部类ReadLock和WriteLock,还有包含继承AQS的Sync内部类,以及两个子实现类FairSync和NonfairSync。

在这里插入图片描述

2、构造方法解析

  1. 默认构造方法使用的是非公平锁模式
  2. 有参构造方法初始化了读锁和写锁,并且根据传入的参数确定使用公平锁和非公平锁。

在这里插入图片描述

3、重要方法解析

3.1、ReadLock#lock(加读锁)

这里直接委托给Sync对象,调用其acquireShared方法

在这里插入图片描述

3.1.1、AbstractQueuedSynchronizer#acquireShared

  1. 调用tryAcquireShared方法尝试获取共享锁,这里是模版方法,会调用具体实现的sync类的方法
  2. 如果尝试获取共享锁不成功,则会调用doAcquireShared方法加入队列

在这里插入图片描述

3.1.1.1、NonfairSync#tryAcquireShared

这里会直接调用父类Sync的nonfairTryAcquireShared方法

在这里插入图片描述

3.1.1.1.1、Sync#tryAcquireShared
  1. 这里首先判断有没有别人获取了写锁,并且这个获取写锁的人不是自己那么加锁失败,因为读写锁是读写互斥的,这里把state这个int变量分成高16位和低16位,高16位表示当前读锁的占有量,低16位表示写锁的占有量。
  2. 根据策略判断读锁是否需要阻塞,这个是根据公平或者非平的策略,在公平锁的情况下应该都过去排队,在非公平的情况下读读不是互斥的,可以直接获取锁,这样在非公平的情况下写锁可能一致拿不到锁导致写饥饿,所以这里为了防止写锁一直拿不到锁,所以判断当前队列的第一个如果是写锁的话,那么读锁也要去排队。然后判断读锁有没有到达上限,如果都没问题则会尝试使用CAS修改state变量,修改成功说明获取读锁陈工,则会记录一下当前读锁的数量。
  3. 如果上面使用CAS修改失败,没有抢到锁,则会调用fullTryAcquireShared方法循环进行获取锁

在这里插入图片描述

3.1.1.1.2、Sync#fullTryAcquireShared

这里就会阻塞在一个死循环中,内部的逻辑和外面获取锁一致,会校验是否有别人拿到了写锁,是否应该阻塞,是否读锁到达了上限,都没命中则使用CAS修改当前state的状态。

在这里插入图片描述

3.1.1.2、AbstractQueuedSynchronizer#doAcquireShared

如果上面调用tryAcquireShared方法没有获取到共享锁,可能当前有写锁,可能读锁到上限了,可能因为公平策略导致要去队列排队都会进入到这个方法,在这个方法中逻辑如下:

  1. 把当前线程包装成一个共享节点
  2. 在一个死循环中park自己,如果被前驱节点唤醒,则会判断前一个节点是否是头节点,如果是尝试去抢占锁,如果抢占不到则继续进行阻塞。
  3. 如果获取到锁了则会带调用setHeadAndPropagate方法设置头节点并传播,传播及唤醒下一个读节点(如果下一个节点是读节点的话)

在这里插入图片描述

3.1.1.2.1、AbstractQueuedSynchronizer#setHeadAndPropagate
  1. 设置当前节点为头节点
  2. 如果旧的头节点或新的头节点为空或者其等待状态小于0(表示状态为SIGNAL/PROPAGATE),则如果下一个节点是读节点则唤醒下一个节点。

在这里插入图片描述

3.1.1.2.2、AbstractQueuedSynchronizer#doReleaseShared

这里可以看到这个方法中之后唤醒一个后继节点,这里采用一个节点唤醒一个节点的形式,不是一个节点获取了读锁一次性唤醒后续的所有读节点。

在这里插入图片描述

3.2、ReadLock#unlock(释放读锁)

调用Sync#releaseShared方法

在这里插入图片描述

3.2.1、AbstractQueuedSynchronizer#releaseShared

  1. 调用tryReleaseShared释放共享锁
  2. 调用doReleaseShared唤醒下一个节点

在这里插入图片描述

3.2.1.1、Sync#tryReleaseShared
  1. 这里修改读线程计数器减减
  2. 在循环中修改state,让共享锁获取次数减1,如果共享锁次数减成0了,那么说明完全释放了,返回true,就唤醒下一个节点。

在这里插入图片描述

3.2.1.2、AbstractQueuedSynchronizer#doReleaseShared

这里如果后继有节点,则会调用unparkSuccessor方法唤醒下一个节点。

在这里插入图片描述

3.2.1.2.1、AbstractQueuedSynchronizer#unparkSuccessor

这里最终会调用unpark方法唤醒下一个状态正常的节点。

在这里插入图片描述

3.3、WriteLock#lock(加写锁)

委托给Sync#acquire方法

在这里插入图片描述

3.3.1、AbstractQueuedSynchronizer#acquire方法

这里我们看下.ReentrantReadWriteLock.Sync.tryAcquire()的实现,至于后续没有获取到锁去队列排序的逻辑就和ReentrantLock一致了,这个我们之前剖析过。

在这里插入图片描述

3.3.1、Sync#tryAcquire方法
  1. 如果有当前有线程持有读锁,则尝试获取写锁失败
  2. 如果有其他线程占有写锁则获取写锁失败
  3. 如果是当前线程占有写锁,那么state值+1,是可重入锁
  4. 如果没有线程占有锁,则尝试修改state的值,成功了则返回获取锁成功
  5. 失败了则后续进入队列排队。

在这里插入图片描述

3.4、WriteLock#unlock(释放写锁)

委托给sync#release方法

在这里插入图片描述

3.4.1、AbstractQueuedSynchronizer#release

  1. 这里会调用到.ReentrantReadWriteLock.Sync.tryRelease来释放锁
  2. 唤醒后继节点,这个和ReentrantLock流程一致,

在这里插入图片描述

3.4.2、Sync#release

  1. 先尝试释放锁,把state的值减1
  2. 如果减为0了,则说明完全释放锁了,完全释放锁了则返回true,说明可以唤醒下一个等待节点。

在这里插入图片描述


总结

  1. ReentrantReadWriteLock提供了读写锁,适用于读多写少的场景,提高吞吐量
  2. 读写锁读读不互斥,读写互斥
  3. 当一个读线程获取锁的时候,不是一次性唤醒后续等待的所有读线程,而是接力一个接一个唤醒的,可能是为了防止同时过多的上下文切换
  4. 只有多个读锁都完全释放了才会唤醒下一个写线程,这种场景下可能会导致写线程饥荒,因为一直存在读线程。
  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值