【源码解析】聊聊ReentrantReadWriteLock是如何实现的读写锁

为什么需要读写锁

在并发编程领域,有多线程进行提升整体性能,但是却引入了共享数据安全性问题。基本就是无锁编程下的单线程操作,有互斥同步锁操作,但是性能不高,并且同一时刻只有一个线程可以操作资源类。但是对于大多数常见下,都是读操作多,写操作少,那么可以利用将锁的粒度进行细化,进而分化出读锁/写锁。也就是syn/ReentrantLock的升级版本ReentrantReadWriteLock。

之前一篇文章已经简单介绍过 ,本篇主要从源码角度剖析具体原理如何实现的。
聊聊ReentrantReadWriteLock锁降级和StampedLock邮戳锁

源码解析

带着三个问题去梳理

  • 读写锁是怎样实现分别记录读写锁的状态?
  • 读锁如何获取和释放锁
  • 写锁如何获取和释放锁

在这里插入图片描述
可以看到顶层通过接口定义规范,内部持有Sync实现AQS,分别实现不同的公平锁和非公平锁。
在这里插入图片描述

//读写锁的接口规范
public interface ReadWriteLock {Lock readLock();Lock writeLock();
}
// 内部持有读写锁 
public class ReentrantReadWriteLockimplements ReadWriteLock, java.io.Serializable {private static final long serialVersionUID = -6992448646407690164L;private final ReentrantReadWriteLock.ReadLock readerLock;private final ReentrantReadWriteLock.WriteLock writerLock;final Sync sync;
    public ReentrantReadWriteLock() {this(false);}

默认是非公平锁。内部通过构造方法创建两个锁,读锁和写锁。

    public ReentrantReadWriteLock(boolean fair) {sync = fair ? new FairSync() : new NonfairSync();readerLock = new ReadLock(this);writerLock = new WriteLock(this);}

锁状态

看到这里其实有点懵逼,什么 这都是什么操作,其实在AQS内部通过一个变量state进行控制是否可以获取资源,但是读写锁如何要用两个变量的话,其实不太好,所以就通过高16位代表读锁的状态、低16位代表写锁的状态。

对于低16来说,值等于0没有加写锁,值等于1 加了写锁,大于1 标识写锁的重入次数。
高16来说,0 :没有加读锁, 1: 加读锁。 值大于1 不表示读锁的重入次数,表示读锁总共被获取了多少次。读锁的重入次数存储在和线程相关的地方,通过threadLocal进行存储。
在这里插入图片描述

  abstract static class Sync extends AbstractQueuedSynchronizer {private static final long serialVersionUID = 6317671515068378041L;// 偏移位数static final int SHARED_SHIFT   = 16;// 共享锁基本单位  左移16位 state+= shared_unitstatic final int SHARED_UNIT    = (1 << SHARED_SHIFT);// 读锁、写锁 可重入最大数量static final int MAX_COUNT      = (1 << SHARED_SHIFT) - 1;// 获取低16位的条件static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;/** Returns the number of shared holds represented in count  */// 多少线程持有读锁static int sharedCount(int c)    { return c >>> SHARED_SHIFT; }/** Returns the number of exclusive holds represented in count  */// 写锁 是否持有 1 为一个线程持有 2 1次冲入 1次获取写锁static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }

写状态,等于 S & 0x0000FFFF(将高 16 位全部抹去)。 当写状态加1,等于S+1.
读状态,等于 S >>> 16 (无符号补 0 右移 16 位)。当读状态加1,等于 S+(1<<16),也就是S+0x00010000。

这样 我们就完成了一个state值可以同时表示两种状态的。

写锁

写锁加锁

        public void lock() {sync.acquire(1);}

调用AQS的获取

    public final void acquire(int arg) {//tryAcquire(arg) true 获取锁成功直接结束//如果没有获取到锁,acquireQueued 会将线程压入队列中//!tryAcquire(arg)  没有获取到锁,将当前线程挂起//addWaiterif (!tryAcquire(arg) &&acquireQueued(addWaiter(Node.EXCLUSIVE), arg))selfInterrupt();}

ReentrantReadWriteLock内部实现了tryAcquire方法。
该方法主要的作用就是
1.获取当前线程
2.判断state的状态。 c = 0 说明当前没有读锁和写锁,通过CAS进行设置state的值 直接获取锁
3.state值不等于0,w == 0 说明当前有读锁 获取锁失败,返回
4.w != 0 说明 当前是写锁重入,所以判断是否最大值,设置state的值+1
writerShouldBlock() 方法会根据是否是公平锁进行排队处理

       protected final boolean tryAcquire(int acquires) {// 获取当前线程Thread current = Thread.currentThread();// 获取state的值int c = getState();int w = exclusiveCount(c);// c = 0 说明 当前没有读锁和写锁if (c != 0) {// w == 0 等于0 说明 说明当前有读锁  或者当前线程不等于持有锁的线程// 写读互斥if (w == 0 || current != getExclusiveOwnerThread())return false;// 获取写锁 不大于最大值if (w + exclusiveCount(acquires) > MAX_COUNT)throw new Error("Maximum lock count exceeded");// Reentrant acquire// 设置当前值 说明可重入setState(c + acquires);return true;}// 是否需要阻塞 公平锁if (writerShouldBlock() ||//CAS 设置c的值 c += 1!compareAndSetState(c, c + acquires))return false;// 设置为当前线程setExclusiveOwnerThread(current);return true;}

在这里插入图片描述

在这里插入图片描述

写锁释放锁

当当前线程执行完毕业务逻辑之后,就会释放锁。

        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;}

释放锁的流程主要就是
1.判断持有锁的线程是否属于当前线程,不是直接异常
2.将state-1 ,state = 0的话,说明重入的锁释放完毕。清空
3.设置state的值,可能是-1 或者 为0。

        protected final boolean tryRelease(int releases) {// 持有锁的线程 是否等于当前线程if (!isHeldExclusively())throw new IllegalMonitorStateException();// 将当前state -= 1int nextc = getState() - releases;boolean free = exclusiveCount(nextc) == 0;// 如果写锁为0 说明当前没有锁持有了if (free)// 将当前线程释放setExclusiveOwnerThread(null);// 设置state的值setState(nextc);return free;}

在这里插入图片描述

读锁

读锁加锁

        public void lock() {sync.acquireShared(1);}
    public final void acquireShared(int arg) {if (tryAcquireShared(arg) < 0)doAcquireShared(arg);}
        protected final int tryAcquireShared(int unused) {// 获取当前线程Thread current = Thread.currentThread();int c = getState();//判断是否有写锁,并且当前线程不是持有写锁线程if (exclusiveCount(c) != 0 &&getExclusiveOwnerThread() != current)return -1;// 获取读锁int r = sharedCount(c);// 是否需要阻塞if (!readerShouldBlock() &&//是否小于最大值r < MAX_COUNT &&//CAS 设置  高16位加1compareAndSetState(c, c + SHARED_UNIT)) {// 第一次获取读锁if (r == 0) {//设置第一个获取读锁的线程firstReader = current; // 当前线程//设置第一个获取读锁线程的重入数firstReaderHoldCount = 1; //} else if (firstReader == current) {// 如果当前线程是第一个获取读锁的线程,重入数++firstReaderHoldCount++;} else {//刷新除获取锁的第一个读线程的重入数// threadLocal进行记录线程重入次数HoldCounter rh = cachedHoldCounter;if (rh == null || rh.tid != getThreadId(current))cachedHoldCounter = rh = readHolds.get();else if (rh.count == 0)readHolds.set(rh);rh.count++;}return 1;}// 再次尝试获取读锁,return fullTryAcquireShared(current);}

在这里插入图片描述
从这里可以看到,支持锁降级,持有写锁的线程,可以获取读锁,但是后续要记得把读锁和写锁读释放

读锁释放锁

        public void unlock() {sync.releaseShared(1);}
    public final boolean releaseShared(int arg) {if (tryReleaseShared(arg)) {doReleaseShared();return true;}return false;}
        protected final boolean tryReleaseShared(int unused) {Thread current = Thread.currentThread();// 如果当前线程是第一个获取读锁的线程if (firstReader == current) {// 第一个获取读锁的线程 重入次数等于=1// assert firstReaderHoldCount > 0;if (firstReaderHoldCount == 1)//第一个获取读锁的线程设置为nullfirstReader = null;else// 当前线程重入多次 -1firstReaderHoldCount--;//如果不是第一个获取读锁的线程,获取该线程的锁重入次数对象} else {// 获取线程持有共享锁的数量对象HoldCounter rh = cachedHoldCounter;// 如果rh==null 当前线程不是共享锁数量对象对应的线程idif (rh == null || rh.tid != getThreadId(current))//从线程上线文获取,并覆盖rh = readHolds.get();//获取读锁重入数int count = rh.count;if (count <= 1) {readHolds.remove();if (count <= 0)throw unmatchedUnlockException();}--rh.count;}//CAS同步更新for (;;) {int c = getState();int nextc = c - SHARED_UNIT;if (compareAndSetState(c, nextc))// Releasing the read lock has no effect on readers,// but it may allow waiting writers to proceed if// both read and write locks are now free.return nextc == 0;}}

线程读锁的重入数与读锁数量是两个概念,线程读锁的重入数是每个线程获取同一个读锁的次数,读锁数量则是所有线程的读锁重入数总和。举一个例子就是 3个线程 分别获取了3次读锁,那么读锁数量就是9,每个线程的读锁重入数就是3。

锁升级&锁降级

锁升级就是线程持有读锁的前提下,去升级为写锁,显然这是违背读写互斥的。
在这里插入图片描述
锁降级,线程持有写锁的前提下,降级为读锁。
在这里插入图片描述
好了我们来看为什么需要锁降级,如果说针对一块临界区直接加一把大锁,那么其实并发读很低,那么可不可以在获取写锁的前提下 降级为读锁,这样既保证数据的一致性,又可以提升整体的并发度。锁降级就是为了结局这个问题。
在这里插入图片描述

设计思想

通过本篇的大概学习,我们了解到RRW中几个设计要点,通过一个变量去控制两个读写锁的状态,位运算的方式。值得我们借鉴,另一种就是锁降级的为了保证数据安全。以及在整体的代码实现上大量使用模板模式,AQS的子类都是相同的方式。

在这里插入图片描述

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

《PySpark大数据分析实战》-16.云服务模式Databricks介绍运行案例

&#x1f4cb; 博主简介 &#x1f496; 作者简介&#xff1a;大家好&#xff0c;我是wux_labs。&#x1f61c; 热衷于各种主流技术&#xff0c;热爱数据科学、机器学习、云计算、人工智能。 通过了TiDB数据库专员&#xff08;PCTA&#xff09;、TiDB数据库专家&#xff08;PCTP…

操作系统系列:Unix进程系统调用fork,wait,exec

操作系统系列&#xff1a;Unix进程系统调用 fork系统调用fork()运用的小练习 wait系统调用Zombiesexec 系列系统调用 开发者可以查看创建新进程的系统调用&#xff0c;这个模块会讨论与进程相关的Unix系统调用&#xff0c;下一个模块会讨论Win32 APIs相关的进程。 fork系统调用…

【QT】C++/Qt使用Qt自带工具windeployqt打包

基本操作 运行项目debug或者release 将运行后的可执行文件单独放到一个文件夹中 根据项目使用的kits来选择Qt的打包工具 打开工具后移动到exe文件夹下执行windeployqt xxx.exe 预览图 问题 打包后再其他电脑上运行出现下图错误 将自己电脑的这个文件拷到可执行文件夹中既…

word增加引用-endnote使用

使用软件&#xff1a; web of science https://webofscience.clarivate.cn/wos/alldb/basic-search; Pub Med等数据库endnote20 链接: https://pan.baidu.com/s/1VQMEsgFY3kcpCNfIyqEjtQ?pwdy1mz 提取码: y1mz 复制这段内容后打开百度网盘手机App&#xff0c;操作更方便哦 --…

【计算机网络】TCP心跳机制、TCP粘包问题

创作不易&#xff0c;本篇文章如果帮助到了你&#xff0c;还请点赞 关注支持一下♡>&#x16966;<)!! 主页专栏有更多知识&#xff0c;如有疑问欢迎大家指正讨论&#xff0c;共同进步&#xff01; 更多计算机网络知识专栏&#xff1a;计算机网络&#x1f525; 给大家跳段…

MySQL的增删改查(进阶)--上

1. 数据库约束 1.1 约束类型 NOT NULL - 指示某列不能存储 NULL 值。 UNIQUE - 保证某列的每行必须有唯一的值。 DEFAULT - 规定没有给列赋值时的默认值。 PRIMARY KEY - NOT NULL 和 UNIQUE 的结合。确保某列&#xff08;或两个列多个列的结合&#xff09;有唯一标识&#xf…

01AVue入门(持续学习中)

1.使用AVue开发简单的前端页面直接简单到起飞,他是Element PlusVueVite开发的,不需要向元素的前端代码一样一个组件要传很多参数,他可以使用Json文本来控制我们要传入的数据结构来决定显示什么 //我使用的比较新,我们也可以使用cdn直接使用script标签直接引入 2.开发中遇到的坑…

【C++】初识模板

本文目录 1. 泛型编程2. 函数模板2.1 函数模板概念2.2 函数模板格式2.3 函数模板的原理2.4 函数模板的实例化2.5 模板参数的匹配原则 3. 类模板3.1 类模板的定义格式3.2 类模板的实例化 1. 泛型编程 如何实现一个通用的交换函数呢&#xff1f; void Swap(int& left, int&…

PYthon Pandas 时间序列数据重采样-resample()方法(第23讲)

PYthon Pandas 时间序列数据重采样-resample()方法(第23讲)         🍹博主 侯小啾 感谢您的支持与信赖。☀️ 🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔…

windows远程桌面怎么开启?

文章目录 如下三种开启方式&#xff0c;任选一即可方式1.在系统属性中开启远程桌面方式2.通过系统设置开启远程桌面方式3.注册表编辑器开启远程桌面使用远程桌面 如下三种开启方式&#xff0c;任选一即可 配合 组网工具或者内网穿透 超级爽 局域网其他pc如何访问宿主机虚拟机IP…

智能优化算法应用:基于未来搜索算法3D无线传感器网络(WSN)覆盖优化 - 附代码

智能优化算法应用&#xff1a;基于未来搜索算法3D无线传感器网络(WSN)覆盖优化 - 附代码 文章目录 智能优化算法应用&#xff1a;基于未来搜索算法3D无线传感器网络(WSN)覆盖优化 - 附代码1.无线传感网络节点模型2.覆盖数学模型及分析3.未来搜索算法4.实验参数设定5.算法结果6.…

C#线程的定义和使用方法

引言 在C#编程语言中&#xff0c;线程是一种并发执行的机制&#xff0c;允许程序同时执行多个任务。线程的使用使得我们能够利用计算机的多核处理器&#xff0c;实现程序的并行执行&#xff0c;提高系统的性能和响应能力。本文将详细介绍C#中线程的定义和使用方法&#xff0c;涵…

【SpringBoot快速入门】(4)SpringBoot项目案例代码示例

目录 1 创建工程3 配置文件4 静态资源 之前我们已经学习的Spring、SpringMVC、Mabatis、Maven&#xff0c;详细讲解了Spring、SpringMVC、Mabatis整合SSM的方案和案例&#xff0c;上一节我们学习了SpringBoot的开发步骤、工程构建方法以及工程的快速启动&#xff0c;从这一节开…

◢Django md5加密与中间件middleware

utils文件夹是重新建立的&#xff08;与migrations同级&#xff09;&#xff0c;该文件夹下主要存放工具&#xff0c;就像static文件夹下只存放静态文件一样 加密 在utils文件夹下建立encrypt.py文件 from django.conf import settings import hashlib def md5(data_string)…

【JavaWeb学习笔记】13 - JSP浏览器渲染技术

JSP 一、JSP引入 1.JSP现状 1.目前主流的技术是前后端分离(比如: Spring Boot Vue/React),我们会讲的.[看一下] 2. JSP技术使用在逐渐减少&#xff0c;但使用少和没有使用是两个意思&#xff0c;一些老项目和中小公司还在使用JSP&#xff0c;工作期间,你很有可能遇到JSP …

手写单链表(指针)(next域)附图

目录 创建文件&#xff1a; 具体实现&#xff1a; 首先是头插。 注意&#xff1a;一定要注意&#xff1a;再定义tmp时&#xff0c;要给它赋一个初始值&#xff08;推荐使用 new list_next) 接着是尾插&#xff1a; 随后是中间插&#xff1a; 然后是最简单的改值&#xf…

Linux笔记---系统信息

&#x1f34e;个人博客&#xff1a;个人主页 &#x1f3c6;个人专栏&#xff1a;Linux学习 ⛳️ 功不唐捐&#xff0c;玉汝于成 目录 前言 命令 1. uname - 显示系统信息 2. hostname - 显示或设置系统主机名 3. top - 显示系统资源使用情况 4. df - 显示磁盘空间使用情…

IDEA Community html文件里的script标签没有syntax highlighting的解决方案

在网上找到的解决方法有的是针对Ultimate版本才可以下载的plugin&#xff0c;对我所用的Community版本无法生效&#xff0c;找了一圈最后在stackoverflow上找到一个有效的方案&#xff0c;给需要的小伙伴分享一下&#xff1a;IntelliJ Community Edition: Javascript syntax hi…

Gemini 1.0:Google推出的全新AI模型,改变生成式人工智能领域的游戏规则!

Gemini 1.0&#xff1a;Google推出的全新AI模型&#xff0c;将改变生成式人工智能领域的游戏规则&#xff01; &#x1f3a5; 屿小夏 &#xff1a; 个人主页 &#x1f525;个人专栏 &#xff1a; IT杂谈 &#x1f304; 莫道桑榆晚&#xff0c;为霞尚满天&#xff01; 文章目录 …

Python之set集合的相关介绍

认识python中的set集合及其用法 python中&#xff0c;集合(set)是一个无序排列&#xff0c;可哈希&#xff0c;支持集合关系测试,不支持索引和切片操作&#xff0c;没有特定语法格式&#xff0c;只能通过工厂函数创建.集合里不会出现两个相同的元素&#xff0c;所以集合常用来…