【源码解析】聊聊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.mzph.cn/news/237208.shtml

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

相关文章

[SWPUCTF 2021 新生赛]gift_F12

打开环境 题目有提示&#xff08;F12&#xff09;&#xff0c;那就查看一下源代码 直接滑到最后 看提示猜测&#xff0c;flag就在源代码里了 ctrlf查找flag 最后得到flag&#xff0c;改一下形式就可以了

ORACLE总结

例1&#xff1a; 现在有 医嘱主表ORDER_MAIN(ID,ORDER_ID,ORDER_NAME) A表 和医嘱执行表ORDER_EXEC(ID,ORDER_ID,PLAN_TIME) B表 两个表是用ORDER_ID关联起来的&#xff0c;A表一个ORDER_ID对应一条数据&#xff0c;B表一个ORDER_ID对应多个字段&#xff0c;要求根据ORDER_…

网络技术基础与计算思维实验教程_2.4_跨交换机VLAN配置实验

实验内容 实验目的 实验原理 实验步骤 构建 在工作区放置交换机然后单击 选择config , 把交换机的默认名改为switch1 再放置两个交换机 再放置终端 放置三台与交换机1相连的终端 再放置三台与交换机3相连的终端 再放置两台与交换机2相连的终端 用直通线连接 然后用交叉线互联交…

java8流库之Stream.iterate

简介 java.util.stream.Stream 下共有两个 iterate iterate(T seed, final UnaryOperator<T> f)iterate(T seed, Predicate<? super T> hasNext, UnaryOperator<T> f) 该方法产生一个无限流&#xff0c;它的元素包含seed&#xff0c;在seed上调用f产生的…

Matlab论文插图绘制模板第131期—函数等高线图

在之前的文章中&#xff0c;分享了Matlab函数折线图的绘制模板&#xff1a; 函数三维折线图&#xff1a; 函数网格曲面图&#xff1a; 函数曲面图&#xff1a; 进一步&#xff0c;再来分享一下函数等高线图。 先来看一下成品效果&#xff1a; 特别提示&#xff1a;本期内容『数…

【Week-P2】CNN彩色图片分类-CIFAR10数据集

文章目录 一、环境配置二、准备数据三、搭建网络结构四、开始训练五、查看训练结果六、总结3.1 ⭐ torch.nn.Conv2d()详解3.2 ⭐ torch.nn.Linear()详解3.3 ⭐torch.nn.MaxPool2d()详解3.4 ⭐ 关于卷积层、池化层的计算4.2.1 optimizer.zero_grad()说明4.2.2 loss.backward()说…

MyBatis Plus使用遇到的问题

如果想使用Mapper的xxxById()方法&#xff0c;实体类的主键上面必须加上TableId注解&#xff0c;如果不加&#xff0c;会报错 2023-12-21 22:48:33.526 WARN 11212 --- [ main] c.b.m.core.injector.DefaultSqlInjector : class com.example.mybatisplusdemo.dom…

ubuntu18.04 64 位安装笔记——备赛笔记——2024全国职业院校技能大赛“大数据应用开发”赛项——任务2:离线数据处理

进入VirtuakBox官网&#xff0c;网址链接&#xff1a;Oracle VM VirtualBoxhttps://www.virtualbox.org/ 网页连接&#xff1a;Ubuntu Virtual Machine Images for VirtualBox and VMwarehttps://www.osboxes.org/ubuntu/ 将下发的ds_db01.sql数据库文件放置mysql中 12、编写S…

无约束优化问题求解笔记(2):最速下降法

目录 3. 最速下降法3.1 最速下降法的基本思想3.2 基于精确搜索的最速下降法3.3 基于精确搜索的最速下降法的程序实现3.4 基于精确搜索的最速下降法的缺点 Reference 3. 最速下降法 3.1 最速下降法的基本思想 最速下降法是典型的线搜索方法. 设 f f f 是 R n \mathbb{R}^n R…

vue2与vue3的区别

首先了解什么是vue3 Vue.js 3.0&#xff08;简称Vue 3&#xff09;是一个流行的JavaScript框架Vue.js的最新版本。Vue 3于2020年9月正式发布&#xff0c;带来了许多令人兴奋的新特性和改进。下面是Vue 3的一些主要特点&#xff1a; 更快的性能&#xff1a;Vue 3通过全新的响应…

【redis】redis系统实现发布订阅的标准模板

目录 简介参数配置代码模板 简介 Redis发布订阅功能是Redis的一种消息传递模式&#xff0c;允许多个客户端之间通过消息通道进行实时的消息传递。在发布订阅模式下&#xff0c;消息的发送者被称为发布者&#xff08;publisher&#xff09;&#xff0c;而接收消息的客户端被称为…

Easyexcel读取单/多sheet页

Easyexcel读取单/多sheet页 此文档会说明单个和多个的sheet页的读取方法&#xff0c;包括本人在使用过程中的踩坑点。 依赖不会的自行百度导入&#xff0c;话不多说&#xff0c;直接上干货。以下示例基于2.x&#xff0c;新版本基本类似 1、创建实体 实体是用来接收对应列的数据…

【QT】QGraphicsView和QGraphicsItem坐标转换

坐标转换 QGraphicsItem和QGraphicsView之间的坐标转换需要通过QGraphicsScene进行转换 QGraphicsView::mapToScene() - 视图 -> 场景QGraphicsView::mapFromScene() - 场景 -> 视图QGraphicsItem::mapToScene() - 图元 -> 场景QGraphicsItem::mapFromScene() - 场景 …

java连接oracle出现ORA-12505错误

问题 sqlplus可以连接;但java连接报错:ORA-12505 ORA-12505, TNS:listener does not currently know of SID given in connect descr 解析 原因: 数据库中实际使用的实例名并非与集群对外使用的相同&#xff0c;使用第三方构件或程序进行连接的时候&#xff0c;所给数据库运…

Arduino驱动LTR390-UV紫外线传感器(光照传感器篇)

目录 1、传感器特性 2、硬件原理图 3、控制器和传感器连线图 4、驱动程序

C++ Qt开发:StringListModel字符串列表映射组件

Qt 是一个跨平台C图形界面开发库&#xff0c;利用Qt可以快速开发跨平台窗体应用程序&#xff0c;在Qt中我们可以通过拖拽的方式将不同组件放到指定的位置&#xff0c;实现图形化开发极大的方便了开发效率&#xff0c;本章将重点介绍QStringListModel字符串映射组件的常用方法及…

Java-JDBC(重要)--待更新

1、第一个JDBC程序 步骤总结&#xff1a; 创建测试数据库加载驱动&#xff0c;固定写法连接数据库获得执行SQL的对象statement执行SQL对象去执行SQLresultset获得返回结果集释放连接 import java.sql.*;public class Jdbc {public static void main(String args[]) throws C…

线程(四)

线程(一) ~ 线程(四)章节导图 导图https://naotu.baidu.com/file/07f437ff6bc3fa7939e171b00f133e17 线程安全 什么是线程安全&#xff1f; 业务中多线程同时访问一个对象或方法时我们不需要做额外的处理&#xff08;像单线程编程一样&#xff09;程序可以正常运行并能获取…

11_12-Golang中的运算符

**Golang **中的运算符 主讲教师&#xff1a;&#xff08;大地&#xff09; 合作网站&#xff1a;www.itying.com** **&#xff08;IT 营&#xff09; 我的专栏&#xff1a;https://www.itying.com/category-79-b0.html 1、Golang 内置的运算符 算术运算符关系运算符逻辑运…

JS模块化规范之ES6及UMD

JS模块化规范之ES6及总结 前言ES6模块化概念基本使用ES6实现 UMD(Universal Module Definition)总结 前言 ESM在模块之间的依赖关系是高度确定的&#xff0c;与运行状态无关&#xff0c;编译工具只需要对ESM模块做静态分析&#xff0c;就可以从代码字面中推断出哪些模块值未曾被…