JUC 锁之 ReentrantReadWriteLock 详解

一 概述

ReentrantReadWriteLock(后面简称 RRW),我们一般称之为读写锁,主要使用在:读多写少的场景

1.1 读写锁规范

作为合格的读写锁,先要有读锁与写锁才行。

所以声明了 ReadWriteLock 接口,作为读写锁的基本规范。
在这里插入图片描述
之后都是围绕着规范去实现读锁与写锁。

1.2 读锁与写锁

ReadLock 与 WriteLock 就是读锁和写锁,它们是 RRW 实现 ReadWriteLock 接口的产物。

但读锁、写锁也要遵守锁操作的基本规范.

所以 WriteLock 与 ReadLock 都实现了 Lock 接口。
在这里插入图片描述
那么 WriteLock 与 ReadLock 对 Lock 接口具体是如何实现的呢?

自然是少不了我们的老朋友 AQS 了。

1.3 AQS

众所周知,要实现锁的基本操作,必须要仰仗 AQS 。

AQS(AbstractQueuedSynchronizer)抽象类定义了一套多线程访问共享资源的同步模板,解决了实现同步器时涉及的大量细节问题,能够极大地减少实现工作,简而言之,AQS 为加锁和解锁过程提供了统一的模板函数,只有少量细节由子类自己决定。

AQS 简化流程图如下:
在这里插入图片描述
关于 AQS 的详细内容,请参考 AbstractQueuedSynchronizer(AQS) 原理

1.4 Sync

AQS 为加锁和解锁过程提供了统一的模板函数,只有少量细节由子类自己决定,但是 WriteLock 与 ReadLock 没有直接继承 AQS。

因为 WriteLock 与 ReadLock 觉得,自己还要去继承 AQS 实现一些两者可以公用的抽象函数,不仅麻烦,还有重复劳动。

所以干脆单独提供一个对锁操作的类,由 WriteLock 与 ReadLock 持有使用,这个类叫 Sync。

Sync 继承 AQS 实现了如下的核心抽象函数:

  • tryAcquire
  • release
  • tryAcquireShared
  • tryReleaseShared

在这里插入图片描述
其中 tryAcquire、release 是为 WriteLock 写锁准备的。

tryAcquireShared、tryReleaseShared 是为 ReadLock 读锁准备的,这里我们后面会详细介绍。

上面说了 Sync 实现了一些 AQS 的核心抽象函数,但是 Sync 本身也有一些重要的内容,看看下面这段代码:
在这里插入图片描述
我们都知道 AQS 中维护了一个 state 状态变量,正常来说,维护读锁与写锁状态需要两个变量,但是为了节约资源,使用高低位切割实现 state 状态变量维护两种状态,即高 16 位表示读状态,低 16 位表示写状态。

Sync 中还定义了 HoldCounter 与 ThreadLocalHoldCounter

  • HoldCounter 是用来记录读锁重入数的对象
  • ThreadLocalHoldCounter 是 ThreadLocal 变量,用来存放第一个获取读锁线程外的其他线程的读锁重入数对象

在这里插入图片描述

1.5 公平与非公平策略

你看,人家 ReentrantLock 都有公平与非公平策略,所以 ReentrantReadWriteLock 也要有。

什么是公平与非公平策略?

因为在 AQS 流程中,获取锁失败的线程,会被构建成节点入队到 CLH 队列,其他线程释放锁会唤醒 CLH 队列的线程重新竞争锁,如下图所示(简化流程)。
在这里插入图片描述
非公平策略是指,非 CLH 队列的线程与 CLH 队列的线程竞争锁,大家各凭本事,不会因为你是 CLH 队列的线程,排了很久的队,就把锁让给你。

公平策略是指,严格按照 CLH 队列顺序获取锁,一定会让 CLH 队列线程竞争成功,如果非 CLH 队列线程一直占用时间片,那就一直失败,直到时间片轮到 CLH 队列线程为止,所以公平策略的性能会更差。
在这里插入图片描述
回到正题,为了支持公平与非公平策略,Sync 扩展了 FairSync、NonfairSync 子类,两个子类实现了readerShouldBlock、writerShouldBlock 函数,即读锁与写锁是否阻塞。
在这里插入图片描述
关于readerShouldBlock、writerShouldBlock 函数在什么地方使用,我们后面介绍。

二 ReentrantReadWriteLock 创建

ReentrantReadWriteLock 全局图,如下:
在这里插入图片描述
有了全局观后,后面就可以深入细节逐个击破了。

读写锁 ReentrantReadWriteLock 的创建,会初始化化一系列类,代码如下:
在这里插入图片描述
ReentrantReadWriteLock 默认是非公平策略,如果想用公平策略,可以直接调用有参构造器,传入 true 即可。

但不管是创建 FairSync 还是 NonfairSync,都会触发 Sync 的无参构造器,因为 Sync 是它们的父类。
在这里插入图片描述
因为 Sync 需要提供给 ReadLock 与 WriteLock 使用,所以创建 ReadLock 与 WriteLock时,会接收 ReentrantReadWriteLock 对象作为入参。
在这里插入图片描述
最后通过 ReentrantReadWriteLock.sync 把 Sync 交给了 ReadLock 与 WriteLock。

三 获取写锁

我们遵守 ReadWriteLock 接口规范,调用 ReentrantReadWriteLock.writeLock 函数获取写锁对象。
在这里插入图片描述
获取到写锁对象后,遵守 Lock 接口规范,调用 lock 函数获取写锁。

WriteLock.lock 函数是由 Sync 实现的(FairSync 或 NonfairSync)。
在这里插入图片描述
sync.acquire(1) 函数是 AQS 中的独占式获取锁流程模板(Sync 继承自 AQS)。
在这里插入图片描述
WriteLock.lock 调用链如下图:
在这里插入图片描述
我们只关注 tryAcquire 函数,其他函数是 AQS 的获取独占式锁失败后的流程内容,不属于本文范畴,tryAcquire 函数代码如下:
在这里插入图片描述
为了易于理解,我们把它转成流程图:
在这里插入图片描述
通过流程图,我们发现了一些要点:

  • 读写互斥
  • 写写互斥
  • 写锁支持同一个线程重入
  • writerShouldBlock 写锁是否阻塞实现取决公平与非公平的策略(FairSync 和 NonfairSync)

四 释放写锁

获取到写锁,临界区执行完,要记得释放写锁(如果重入多次要释放对应的次数),不然会阻塞其他线程的读写操作,调用 unlock 函数释放写锁(Lock 接口规范)。

WriteLock.unlock 函数也是由 Sync 实现的(FairSync 或 NonfairSync)。
在这里插入图片描述
sync.release(1) 执行的是 AQS 中的独占式释放锁流程模板(Sync 继承自 AQS)。
在这里插入图片描述
WriteLock.unlock 调用链如下图:
在这里插入图片描述
再来看看 tryRelease 函数,其他函数是 AQS 的释放独占式成功后的流程内容,不属于本文范畴,tryRelease 函数代码如下:
在这里插入图片描述
为了易于理解,我们把它转成流程图:
在这里插入图片描述
因为同一个线程可以对相同的写锁重入多次,所以也要释放的相同的次数。

五 获取读锁

我们遵守 ReadWriteLock 接口规范,调用 ReentrantReadWriteLock.readLock 函数获取读锁对象。
在这里插入图片描述
获取到读锁对象后,遵守 Lock 接口规范,调用 lock 函数获取读锁。

ReadLock.lock 函数是由 Sync 实现的(FairSync 或 NonfairSync)。
在这里插入图片描述
sync.acquireShared(1) 函数执行的是 AQS 中的共享式获取锁流程模板(Sync 继承自 AQS)。
在这里插入图片描述
ReadLock.lock 调用链如下图:
在这里插入图片描述
我们只关注 tryAcquireShared 函数,doAcquireShared 函数是 AQS 的获取共享式锁失败后的流程内容,不属于本文范畴,tryAcquireShared 函数代码如下:
在这里插入图片描述
代码挺多的,为了易于理解,我们把它转成流程图:
在这里插入图片描述
通过流程图,我们发现了一些要点

  • 读锁共享,读读不互斥
  • 读锁可重入,每个获取读锁的线程都会记录对应的重入数
  • 读写互斥,锁降级场景除外
  • 支持锁降级,持有写锁的线程,可以获取读锁,但是后续要记得把读锁和写锁读释放
  • readerShouldBlock 读锁是否阻塞实现取决公平与非公平的策略(FairSync 和 NonfairSync)

六 释放读锁

获取到读锁,执行完临界区后,要记得释放读锁(如果重入多次要释放对应的次数),不然会阻塞其他线程的写操作,通过调用unlock函数释放读锁(Lock 接口规范)。

ReadLock.unlock 函数也是由 Sync 实现的(FairSync 或 NonfairSync)。
在这里插入图片描述
sync.releaseShared(1) 函数执行的是 AQS 中的共享式释放锁流程模板(Sync 继承自 AQS)。
在这里插入图片描述
ReadLock.unlock 调用链如下图:
在这里插入图片描述
我们只关注 tryReleaseShared 函数,doReleaseShared 函数是 AQS 的释放共享式锁成功后的流程内容,不属于本文范畴,tryReleaseShared 函数代码如下:
在这里插入图片描述
流程图如下:
在这里插入图片描述
这里有三点需要注意:

  • 第一点:线程读锁的重入数与读锁数量是两个概念,线程读锁的重入数是每个线程获取同一个读锁的次数,读锁数量则是所有线程的读锁重入数总和
  • 第二点:AQS 的共享式释放锁流程模板中,只有全部的读锁被释放了,才会去执行 doReleaseShared 函数
  • 第三点:因为使用的是 AQS 共享式流程模板,如果 CLH 队列后面的线程节点都是因写锁阻塞的读锁线程节点,会传播唤醒

七 总结

ReentrantReadWriteLock 底层实现与 ReentrantLock 思路一致,它们都离不开 AQS,都是声明一个继承 AQS 的 Sync,并在 Sync 下扩展公平与非公平策略,后续的锁相关操作都委托给公平与非公平策略执行。

我们还发现,在 AQS 中除了独占式模板,还有共享式模板,它们在多线程访问共享资源的流程会有所差异,就如 ReentrantReadWriteLock 中读锁使用共享式,写锁使用独占式。

最后再捋一捋写锁与读锁的逻辑:

  • 读读不阻塞
  • 写锁阻塞写之后的读写锁,但是不阻塞写锁之前的读锁线程
  • 写锁会被写之前的读写锁阻塞
  • 读锁节点唤醒会无条件传播唤醒CLH队列后面的读锁节点
  • 写锁可以降级为读锁,防止更新丢失
  • 读锁、写锁都支持重入
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值