初探 JUC 并发编程:读写锁 ReentrantReadWriteLock 原理(8000 字源码详解)

本文中会涉及到一些前面 ReentrantLock 中学到的内容,先去阅读一下我关于独占锁 ReentrantLock 的源码解析阅读起来会更加清晰。
初探 JUC 并发编程:独占锁 ReentrantLock 底层源码解析

6.4)读写锁 ReentrantReadWriteLock 原理

前面提到的 ReentrantLock 是独占锁,某个时间只有一个线程可以获取这个锁,而实际情况中会出现读多写少的情况,ReentrantLock 无法满足这个需求,所以就有了读写锁 ReentrantReadWriteLock。这个锁采用了读写分离的策略,允许多个线程同时获取读锁。

6.4.1)类图结构

在这里插入图片描述

ReentrantReadWriteLock 的类图结构如图所示,类中维护了一个 ReadLock 和 WriteLock,它们依赖 Sync 实现功能,而 Sync 继承自 AQS,也提供了公平和非公平的实现。

下面来看一下 Sync 中的属性和常用方法:

因为读写锁中维护了读锁和写锁两个状态,但是 AQS 只提供了一个 state;读写锁中巧妙的使用了 state 的高 16 位表示读状态,也就是获取到读锁的次数,使用低十六位表示写的次数。

        static final int SHARED_SHIFT   = 16; // 读锁状态单位值 65536static final int SHARED_UNIT    = (1 << SHARED_SHIFT);// 读锁的状态单位值 65536static final int MAX_COUNT      = (1 << SHARED_SHIFT) - 1;// 写锁掩码,15 个 1static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;/** 返回读锁线程数  */static int sharedCount(int c) { // c 一般为 state 的值,将值右移 16 为return c >>> SHARED_SHIFT; }/** 返回写锁的重入次数  */static int exclusiveCount(int c) { // 将值与写锁掩码做与操作return c & EXCLUSIVE_MASK; }
        // 第一个获取到读锁的线程private transient Thread firstReader = null;// 第一个获取到读锁的线程的可重入次数private transient int firstReaderHoldCount;// 记录最后一个获取到读锁的可重入次数private transient HoldCounter cachedHoldCounter;static final class HoldCounter {int count = 0;// 使用 id 而不是引用来避免垃圾保留final long tid = getThreadId(Thread.currentThread());}

其中 readHolds 是一个 ThreadLocal 变量,存放第一个获取到读线程之外的其他线程读锁的可重入次数,ThreadLocalHoldCounter 继承自 ThreadLocal。
firstReader: 这是一个线程引用,用来记录第一个获得读锁的线程。当锁从无读线程持有(即读锁计数器shareCount为0)变为有读线程持有(即读锁计数器shareCount至少为1)时,这个变量会记录下那个“第一个”读线程。

这样做主要是为了优化后续的读锁获取操作,因为一旦有线程成为了firstReader,它在再次尝试获取读锁时,可以更快地进行,因为它不需要像其他线程那样去更新或检查线程局部的HoldCounter对象。如果这个线程释放了它的所有读锁,导致读锁计数器回到0,那么firstReader会被设置为null

而 cachedHoldCounter 是存储最后一个获取到锁的线程的 id 和 count,是为了减少在常见情况下(即最近释放锁的线程通常是最近获取锁的线程)的线程本地存储(ThreadLocal)查找开销。

        private transient ThreadLocalHoldCounter readHolds;static final class ThreadLocalHoldCounterextends ThreadLocal<HoldCounter> {public HoldCounter initialValue() {return new HoldCounter();}}

6.4.2)写锁的获取与释放

        ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();ReentrantReadWriteLock.WriteLock writeLock = readWriteLock.writeLock();

通过上面的代码获取写锁,写锁和上面的 ReentrantLock 锁都是独占可重入锁,所以方法都差不多,调用 lock 方法,可以获取锁:

        public void lock() {sync.acquire(1);}public final void acquire(int arg) {// 调用 sync 中重写的 tryAcquire 方法if (!tryAcquire(arg) &&acquireQueued(addWaiter(Node.EXCLUSIVE), arg))selfInterrupt();}

其中调用了 WriteLock 中重写的 tryAcquire 方法:

        protected final boolean tryAcquire(int acquires) {Thread current = Thread.currentThread();int c = getState();int w = exclusiveCount(c); // 获取写锁的重入次数// 1)读锁或者写锁被占有if (c != 0) {// 1)写锁的重入次数为 0,也就是被读锁占有的情况,如果读锁占有,则 w 不为 0// 2)当前线程不是持有写锁的线程if (w == 0 || current != getExclusiveOwnerThread())return false;// 1)越界的情况// 2)如果能走到这里,说明锁被当前线程持有if (w + exclusiveCount(acquires) > MAX_COUNT)throw new Error("Maximum lock count exceeded");// 重入次数加一setState(c + acquires);return true;}// 这个 writerShouldBlock() 方法是 ReentrantReadWriteLock 中的一个抽象方法,// 用于确定当前线程在尝试获取写锁时是否应该被阻塞,// 具体是因为什么原因阻塞取决于锁的实现和其策略,如果是非公平锁不需要阻塞// 非公平锁有阻塞相关的逻辑if (writerShouldBlock() ||!compareAndSetState(c, c + acquires))return false;setExclusiveOwnerThread(current);return true;

上面的方法是写锁中实现的 tryAcquire 方法,方法的执行流程是这样的:首先回去判断状态值是否不等于 0,如果不等于零则说明读锁或者写锁被占有(读锁和写锁不能同时起作用),然后去判断写锁的重入次数是否为 0,如果为 0 则说明当前锁是读锁,无法获取写锁;如果当前锁是写锁的话,去判断锁是否被线程持有,如果被持有,对重入次数做一个自增;如果当前锁没有被占有,则将修改低 16 位的 state 来表明当前锁是写锁状态,且写锁被占有。

和 ReentrantLock 相同,方法中也提供了 lockInterruptibly() 方法、 tryLock() 方法、 tryLock(long timeout, TimeUnit unit) 作用和 ReentarntLock 完全相同,这里不赘述了。

写锁的释放方法是委托给 Sync 类来做的:

    public void unlock() {sync.release(1);}public final boolean release(int arg) {if (tryRelease(arg)) {Node h = head;if (h != null && h.waitStatus != 0)unparkSuccessor(h);return true;}return false;}

下面来看核心代码 tryRelease 的实现:

        protected final boolean tryRelease(int releases) {// 1)锁未被当前线程持有if (!isHeldExclusively())throw new IllegalMonitorStateException();int nextc = getState() - releases; // 下次修改的值boolean free = exclusiveCount(nextc) == 0; // 如果为 0 则完全释放锁if (free)setExclusiveOwnerThread(null); // 清除持有锁的线程setState(nextc);return free; // 锁是否被线程持有}

6.4.3)读锁的获取与释放

如果当前没有其他线程持有写锁,则当前线程可以获取读锁,AQS 的状态值 state 的高 16 位会增加 1,如果有线程持有写锁的话,获取读锁的线程会被阻塞。

先来看读锁的 lock 方法,同样是委托给 sync 进行的:

    public void lock() {sync.acquireShared(1);}public final void acquireShared(int arg) {if (tryAcquireShared(arg) < 0)doAcquireShared(arg);}

接下来看一下在读锁中实现的核心代码, tryAcquireShared()

        protected final int tryAcquireShared(int unused) {Thread current = Thread.currentThread();int c = getState();// 1)写锁被占有// 2)写锁不被当前线程持有if (exclusiveCount(c) != 0 &&getExclusiveOwnerThread() != current)return -1;// 获取高 16 位的内容int r = sharedCount(c);// 判断获取读锁的时候是否需要被阻塞// 1)本类中的逻辑为判断 AQS 队列中的第一个元素是否在获取写锁// 2)共享锁的获取次数没有达到上限// 3)当前线程修改 sharedCount 成功// 多个线程调用该方法的时候只要一个线程会成功(因为进行 CAS 操作),// 未成功的线程会进入 fullTryAcquireShared 方法if (!readerShouldBlock() &&r < MAX_COUNT &&compareAndSetState(c, c + SHARED_UNIT)) {// 1)没有线程获取到读锁if (r == 0) {firstReader = current;firstReaderHoldCount = 1;// 1)当前线程是第一个获取到读锁的线程} else if (firstReader == current) {firstReaderHoldCount++;} else {HoldCounter rh = cachedHoldCounter; // 最后一个获取到读锁的线程// 1)最后一个获取到读锁的线程为 null// 2)最后一个获取到锁的线程不是当前线程if (rh == null || rh.tid != getThreadId(current))// 将 cachedHoldCounter 设置为当前线程cachedHoldCounter = rh = readHolds.get();// 1)最后一个获取到锁的线程为当前线程else if (rh.count == 0)// 确保 readHolds 被初始化readHolds.set(rh);rh.count++;}return 1;}return fullTryAcquireShared(current);}

上面的代码中首先检查是否有其他线程获取到了写锁,如果有则直接返回 -1,之后会将当前线程放到 AQS 阻塞队列。如果当前获取读锁的线程持有写锁,则可以直接获取读锁,但注意释放锁的时候将两个锁都释放掉。

本类中的 readerShouldBlock() 方法是这样的:

	  // 避免重复获取读锁导致写锁无法被获取的情况final boolean readerShouldBlock() {return apparentlyFirstQueuedIsExclusive();}// 当前 AQS 队列中收个节点请求的是写锁final boolean apparentlyFirstQueuedIsExclusive() {Node h, s;return (h = head) != null &&(s = h.next)  != null &&!s.isShared()         &&s.thread != null;}

当多次获取读锁可能会导致写锁持续被阻塞,所以当发现 AQS 队列中首个节点请求的是写锁的时候,获取读锁的线程暂时阻塞给写锁让步。

因为多个线程只有一个会获取写锁,剩余的情况在 tryAcquireShared() 中并没有被处理

  1. 有线程获取写锁的时候,被阻塞
  2. CAS 操作失败

这时候就调用 fullTryAcquireShared,这个方法会循环自旋的获取读锁:

        final int fullTryAcquireShared(Thread current) {HoldCounter rh = null;for (;;) {int c = getState();// 1)写锁被占有if (exclusiveCount(c) != 0) {// 1)写锁不被当前线程持有if (getExclusiveOwnerThread() != current)return -1;// 1)当前线程应该被阻塞} else if (readerShouldBlock()) {if (firstReader == current) {// assert firstReaderHoldCount > 0;} else {if (rh == null) {rh = cachedHoldCounter;// 1)最后一个获取读锁的线程为空// 或者// 2)最后一个获取到锁的线程未被设置为本线程if (rh == null || rh.tid != getThreadId(current)) {// 判断当前线程是否获取过锁rh = readHolds.get();if (rh.count == 0)// 未获取过的话,清除 readHoldsreadHolds.remove();}}// 当前线程被阻塞了if (rh.count == 0)return -1;}}// 执行到这里说明写锁没有被占有,且当前线程没有被阻塞,可以尝试获取锁if (sharedCount(c) == MAX_COUNT)// 越界的情况throw new Error("Maximum lock count exceeded");// 使用 CAS 操作修改 state,给获取读锁的线程数加一if (compareAndSetState(c, c + SHARED_UNIT)) {// 如果当前锁没有被线程占用if (sharedCount(c) == 0) {firstReader = current;firstReaderHoldCount = 1;// 第一个持有锁的线程为当前线程} else if (firstReader == current) {firstReaderHoldCount++;} else {if (rh == null)rh = cachedHoldCounter;// cachedHoldCounter 为空,或不为当前线程if (rh == null || rh.tid != getThreadId(current))rh = readHolds.get(); // 设置 rhelse if (rh.count == 0)readHolds.set(rh);rh.count++; // 自增cachedHoldCounter = rh; // cache for release}return 1;}}}

上面的方法中,先去判断写锁有没有被占有,如果被占有则直接返回 -1。

然后去判断当前线程是否应该被阻塞,也就是 AQS 队列的队头是不是请求的写锁,然后去判断最后一个获取到锁的线程是不是本线程,如果不是的话,检查线程中的 readHolds 是否为 0(如果为 0 则说明没有获取到锁,如果获取到了锁这里应该置为 1),因为 get 方法会向线程的 ThreadLocal 中添加对象,所以在确定它没有得到锁之后清楚 ThreadLocal 中的内容。

如果上面的代码均通过,说明写锁没有被占有,且当前线程没有被阻塞,可以尝试获取锁,其中获取锁的方法和上面相同。

同样的,读锁中也存在 tryLock 等方法,这里不做过多赘述。

然后来看释放锁的方法,这里的释放锁也是委托给 Sync 类进行的:

    public void unlock() {sync.releaseShared(1);}public final boolean releaseShared(int arg) {if (tryReleaseShared(arg)) {doReleaseShared();return true;}return false;}        

其中核心方法是 Sync 的实现类中实现的 tryReleaseShared 方法:

        protected final boolean tryReleaseShared(int unused) {Thread current = Thread.currentThread();// 1)当前线程是第一个获取到读锁的线程if (firstReader == current) {if (firstReaderHoldCount == 1)firstReader = null;elsefirstReaderHoldCount--;} else {HoldCounter rh = cachedHoldCounter;// 1)当前线程不是最后一个获取到锁的线程if (rh == null || rh.tid != getThreadId(current))rh = readHolds.get();// 检查重入次数int count = rh.count;// 1)锁已经释放完成,可以清除了if (count <= 1) {readHolds.remove();// 如果是 0 表示未获取到锁if (count <= 0)throw unmatchedUnlockException();}--rh.count;}// 减少一次重入次数for (;;) {int c = getState();int nextc = c - SHARED_UNIT;if (compareAndSetState(c, nextc))return nextc == 0;}}

方法中先对线程是否为 firstReader 或者 cachedHoldCounter 做了判断,对其进行特殊的处理,然后检查重入的次数,如果次数小于等于一,则本次释放就将线程持有的读锁全部释放完成,此时删除线程 ThreadLocal 中的内容;最后循环减少可重入次数。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://xiahunao.cn/news/3029265.html

如若内容造成侵权/违法违规/事实不符,请联系瞎胡闹网进行投诉反馈,一经查实,立即删除!

相关文章

Java入门——类和对象(上)

经读者反映与笔者考虑&#xff0c;近期以及往后内容更新将主要以java为主&#xff0c;望读者周知、见谅。 类与对象是什么&#xff1f; C语言是面向过程的&#xff0c;关注的是过程&#xff0c;分析出求解问题的步骤&#xff0c;通过函数调用逐步解决问题。 JAVA是基于面向对…

C++常用库函数——strcmp、strchr

1、strcmp&#xff1a;比较两个字符串的值是否相等 例如 char a1[6] "AbDeG",*s1 a1;char a2[6] "AbdEg",* s2 a2;s1 2;s2 2;printf("%d \n", strcmp(s1, s2));return(0); s1指向a1&#xff0c;s2指向a2&#xff0c;strcmp表示比较s1和s…

万物生长大会 | 创邻科技再登杭州准独角兽榜单

近日&#xff0c;由民建中央、中国科协指导&#xff0c;民建浙江省委会、中国投资发展促进会联合办的第八届万物生长大会在杭州举办。 在这场创新创业领域一年一度的盛会上&#xff0c;杭州市创业投资协会联合微链共同发布《2024杭州独角兽&准独角兽企业榜单》。榜单显示&…

数据库被攻击后出现1044 - access denied for user ‘root‘@‘% ‘ to database table

MySQL数据库被攻击后&#xff0c;数据库全部被删除&#xff0c;并且加一个一个勒索的数据&#xff0c;向我索要btc&#xff0c; 出现这个问题就是我的数据库密码太简单了&#xff0c;弱密码&#xff0c;被破解了&#xff0c;并且把我权限也给修改了 导致我操作数据库时&#…

vs2019 里 C++ 20规范的 string 类的源码注释

&#xff08;1&#xff09;读源码&#xff0c;可以让我们更好的使用这个类&#xff0c;掌握这个类&#xff0c;知道咱们使用了库代码以后&#xff0c;程序大致具体是怎么执行的。而不用担心程序出不知名的意外的问题。也便于随后的代码调试。 string 类实际是 库中 basic_strin…

国产开源物联网操作系统

软件介绍 RT-Thread是一个开源、中立、社区化发展的物联网操作系统&#xff0c;采用C语言编写&#xff0c;具有易移植的特性。该项目提供完整版和Nano版以满足不同设备的资源需求。 功能特点 1.内核层 RT-Thread内核包括多线程调度、信号量、邮箱、消息队列、内存管理、定时器…

Bokeh实战高级教程:用滑块控件打造动态数据可视化

在数据可视化的世界里&#xff0c;Bokeh无疑是一颗璀璨的明星。它不仅提供了丰富的图表类型&#xff0c;还支持强大的交互功能。今天&#xff0c;我们就来深入探讨如何使用Bokeh的滑块控件&#xff0c;轻松实现数据的动态展示。 首先&#xff0c;让我们从创建ColumnDataSource开…

纯血鸿蒙APP实战开发——阅读翻页方式案例

介绍 本示例展示手机阅读时左右翻页&#xff0c;上下翻页&#xff0c;覆盖翻页的功能。 效果图预览 使用说明 进入模块即是左右翻页模式。点击屏幕中间区域弹出上下菜单。点击设置按钮&#xff0c;弹出翻页方式切换按钮&#xff0c;点击可切换翻页方式。左右翻页方式可点击翻…

C++:多态-重写和重载

重写&#xff08;Override&#xff09;和重载&#xff08;Overload&#xff09;是面向对象编程中常用的两个概念&#xff0c;它们虽然都涉及到方法的定义&#xff0c;但是在实现和使用上有着不同的特点。 重写&#xff08;Override&#xff09;&#xff1a; 重写是指在子类中重…

Python图形复刻——绘制母亲节花束

各位小伙伴&#xff0c;好久不见&#xff0c;今天学习用Python绘制花束。 有一种爱&#xff0c;不求回报&#xff0c;有一种情&#xff0c;无私奉献&#xff0c;这就是母爱。祝天下妈妈节日快乐&#xff0c;幸福永远&#xff01; 图形展示&#xff1a; 代码展示&#xff1a; …

论文解读--------FedMut: Generalized Federated Learning via Stochastic Mutation

动机 Many previous works observed that the well-generalized solutions are located in flat areas rather than sharp areas of the loss landscapes. 通常&#xff0c;由于每个本地模型的任务是相同的&#xff0c;因此每个客户端的损失情况仍然相似。直观上&#xff0c;…

洗地机挑选有哪些要点?附618热门洗地机推荐

随着科技的不断发展&#xff0c;洗地机已经成为了人们家庭里必备的清洁家电了&#xff0c;它可以让我们高效的完成深度清洁的工作&#xff0c;让我们从繁重的家务劳动中解放出来&#xff0c;享受更轻松舒适的生活。那么我们如何在众多洗地机品牌中找到适合自己的产品呢&#xf…

2023盘古石杯晋级赛 apk分析 WP

1. 涉案应用刷刷樂的签名序列号是[答案&#xff1a;123ca12a] 2. 涉案应用刷刷樂是否包含读取短信权限 无 3. 涉案应用刷刷樂打包封装的调证ID值是[答案&#xff1a;123ca12a] 4. 涉案应用刷刷樂服务器地址域名是[答案&#xff1a;axa.baidun.com] 代理模式抓个包 5. 涉案应用…

Android项目转为鸿蒙,真就这么简单?

最近做了一个有关Android转换成鸿蒙的项目。经不少开发者的反馈&#xff1b;许多公司的业务都增加了鸿蒙板块。 对此想分享一下这个项目转换的流程结构&#xff0c;希望能够给大家在工作中带来一些帮助。转换流程示意图如下&#xff1a; 下面我就给大家介绍&#xff0c;Android…

Spring框架学习笔记(一):Spring基本介绍(包含IOC容器底层结构)

1 官方资料 1.1 官网 https://spring.io/ 1.2 进入 Spring5 下拉 projects, 进入 Spring Framework 进入 Spring5 的 github 1.3 在maven项目中导入依赖 <dependencies><!--加入spring开发的基本包--><dependency><groupId>org.springframework<…

多址通信方式的抗噪声性能和系统容量对比

&#x1f3c6;本文收录于「Bug调优」专栏&#xff0c;主要记录项目实战过程中的Bug之前因后果及提供真实有效的解决方案&#xff0c;希望能够助你一臂之力&#xff0c;帮你早日登顶实现财富自由&#x1f680;&#xff1b;同时&#xff0c;欢迎大家关注&&收藏&&…

手撕C语言题典——移除链表元素(单链表)

目录 前言 一.思路 1&#xff09;遍历原链表&#xff0c;找到值为 val 的节点并释放 2&#xff09;创建新链表 二.代码实现 1)大胆去try一下思路 2&#xff09;竟然报错了&#xff1f;&#xff01; 3&#xff09;完善之后的成品代码 搭配食用更佳哦~~ 数据结构之单…

mysql集群NDBcluster引擎在写入数据时报错 (1114, “The table ‘ads‘ is full“)

问题描述&#xff1a;mysql集群在写入数据时&#xff0c;出现上述报错 问题原因&#xff1a;表数据已满&#xff0c;一般是在集群的管理节点设置里面datamemory的值太小&#xff0c;当数据量超过该值时就会出现该问题 解决方案&#xff1a; 修改集群管理节点的config.ini里面…

DS:顺序表、单链表的相关OJ题训练(2)

欢迎各位来到 Harper.Lee 的学习世界&#xff01; 博主主页传送门&#xff1a;Harper.Lee的博客主页 想要一起进步的uu欢迎来后台找我哦&#xff01; 一、力扣--141. 环形链表 题目描述&#xff1a;给你一个链表的头节点 head &#xff0c;判断链表中是否有环。如果链表中有某个…

burp靶场xss漏洞(初级篇)

靶场地址 http://portswigger.net/web-security/all-labs#cross-site-scripting 第一关&#xff1a;反射型 1.发现搜索框直接注入payload <script>alert(111)</script> ​ 2.出现弹窗即说明攻击成功 ​ 第二关&#xff1a;存储型 1.需要在评论里插入payload …