ReentrantReadWriteLock源码分析

文章目录

  • 概述
  • 一、状态位设计
  • 二、读锁
  • 三、锁降级机制
  • 四、写锁
  • 总结


概述

  ReentrantReadWriteLock(读写锁)是对于ReentranLock(可重入锁)的一种改进,在可重入锁的基础上,进行了读写分离。适用于读多写少的场景,对于读取写入操作分别加锁。其中读取与读取操作同步,读取和写入,写入和写入操作互斥。并且支持写锁降级的机制。
  ReentrantReadWriteLock的体系结构:

  • 实现了ReadWriteLock接口,定义了读锁和写锁的模版方法,在ReentrantReadWriteLock分别进行实现(ReentrantReadWriteLock内部实现了两把锁)。
  • sync属性,实现了AQS的规范。
  • ReadLock和WriteLock都实现了Lock接口,Lock接口作为可重入锁的模版,定义了共有的行为。

在这里插入图片描述
在这里插入图片描述实现了读写锁的规范接口

在这里插入图片描述sync属性,是一个静态内部类,继承了AQS,AQS是一种规范,抽象的队列式同步器
在这里插入图片描述MESA管程模型,入口等待队列用于互斥,条件变量等待队列用于同步
在这里插入图片描述ReentrantReadWriteLock中的写锁,实现了Lock接口
在这里插入图片描述ReentrantReadWriteLock中的读锁,同样也实现了Lock接口
在这里插入图片描述Lock接口,规范了锁的实现

在这里插入图片描述ReentrantReadWriteLock 同样支持公平锁和非公平锁的实现

一、状态位设计

  ReentrantReadWriteLock的内部类Sync,重写了父类AQS的acquireShared方法,定义了读锁的共享、可重入特性,但是AQS的state状态位,只能表示同步状态,不能同时维护读锁、写锁的状态。在读写锁的实现中,对于state状态位,实现了按位切割的算法:

  • 低 16 位(低 2 字节):存储 写锁的重入次数
  • 高 16 位(高 2 字节):存储 读锁的获取次数

  为什么写锁存储的是重入次数?写锁通常是互斥的,但有时一个线程可能会多次请求写锁(即重入)所以需要统计的是,某个线程重入了几次写锁。读锁存储的是获取次数?读锁是同步的,一般不会某个线程多次请求读锁,所以需要统计的是当前有多少个线程持有读锁

  Sync类中,定义了状态位的相关属性,以及获取读,写计数的方法:

		// 读锁偏移 16 位,读锁存储在 state 的高 16 位。static final int SHARED_SHIFT   = 16;// 读锁的单位值(1 左移 16 位,即 65536)static final int SHARED_UNIT    = (1 << SHARED_SHIFT);//最大值 (1 左移 16 位 - 1,即 65535)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  *///从 state 变量中提取高 16 位,即 读锁的获取次数。static int sharedCount(int c)    { return c >>> SHARED_SHIFT; }/** Returns the number of exclusive holds represented in count  */// 按位与 & 操作 提取 低 16 位,即 写锁的重入次数。static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }

  用一个案例进行说明:

  1. 假设初始 state = 0(没有读锁和写锁)

state = 0x00000000 (0000 0000 0000 0000 0000 0000 0000 0000)

  • 写锁(低 16 位):exclusiveCount(state) = state & 0000 0000 0000 0000 1111 1111 1111 1111 = 0
  • 读锁(高 16 位):sharedCount(state) = state >>> 16 = 0
  1. 假设写锁重入 3 次,此时 state = 3

state = 0x00000003 (0000 0000 0000 0000 0000 0000 0000 0011)

  • 写锁(低 16 位):

exclusiveCount(state) = state & 0000 0000 0000 0000 1111 1111 1111 1111
= 00000000 00000000 00000000 00000011 & 0000 0000 0000 0000 1111 1111 1111 1111
= 00000000 00000000 00000000 00000011 (3)

  • 读锁(高 16 位):

sharedCount(state) = state >>> 16
= 00000000 00000000 00000000 00000011 >>> 16
= 00000000 00000000 00000000 00000000 (0)

二、读锁

  读锁的特点是共享,也就是读锁和读锁之间不互斥,并且可重入。那么在源码的层面,是如何定义共享、可重入的?
在这里插入图片描述采用sync的acquireShared

  案例代码:

public class Demo1 {private static final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();private static final ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock();private static final ReentrantReadWriteLock.ReadLock readLock = lock.readLock();public static void main(String[] args) {readLock.lock();System.out.println("第一次获取到读锁");try {readLock.lock();System.out.println("第二次获取到读锁");try {System.out.println("重复获取读锁");}finally {readLock.unlock();System.out.println("第二次获取到读锁被释放");}}finally {readLock.unlock();System.out.println("第一次获取到读锁被释放");}}
}

在这里插入图片描述

  读锁的加锁操作:

     protected final int tryAcquireShared(int unused) {/** Walkthrough:* 1. If write lock held by another thread, fail.* 2. Otherwise, this thread is eligible for*    lock wrt state, so ask if it should block*    because of queue policy. If not, try*    to grant by CASing state and updating count.*    Note that step does not check for reentrant*    acquires, which is postponed to full version*    to avoid having to check hold count in*    the more typical non-reentrant case.* 3. If step 2 fails either because thread*    apparently not eligible or CAS fails or count*    saturated, chain to version with full retry loop.*///获取当前线程Thread current = Thread.currentThread();//获取当前线程的状态标记int c = getState();//已经有了写锁,并且写锁的拥有者不是当前线程if (exclusiveCount(c) != 0 &&getExclusiveOwnerThread() != current)//当前有写锁且不是当前线程持有的写锁,返回 -1,表示获取读锁失败。return -1;//得到读锁的获取次数int r = sharedCount(c);//检查当前线程是否需要等待 //确保当前读锁的获取次数 r 小于最大限制 MAX_COUNT//使用 CAS 操作来更新 state。if (!readerShouldBlock() &&r < MAX_COUNT &&compareAndSetState(c, c + SHARED_UNIT)) {//读锁的获取次数为0if (r == 0) {//标记当前线程为第一个获取到的firstReader = current;//记录第一个获取到读锁的线程的获取次数firstReaderHoldCount = 1;//读锁的获取次数不为零,并且当前线程和第一个获取到读锁的线程相同    } else if (firstReader == current) {//第一个获取到读锁的线程的获取次数增加firstReaderHoldCount++;//读锁的获取次数不为零,并且当前线程不是第一个获取到读锁的线程       } else {//从缓存中获取持有计数器HoldCounter rh = cachedHoldCounter;//如果缓存为空或者当前线程的 tid 不匹配if (rh == null || rh.tid != getThreadId(current))//则从 readHolds 中获取新的计数器。cachedHoldCounter = rh = readHolds.get();//如果 rh.count == 0,说明当前线程刚开始持有读锁。else if (rh.count == 0)//更新计数器。readHolds.set(rh);//增加当前线程的读锁计数。rh.count++;}return 1;}//如果前面的 CAS 操作失败,进行 排队等待,直到条件满足为止。return fullTryAcquireShared(current);}

  这一段尝试获取读锁的代码,精髓在于使用 CAS 操作来更新 state,并且记录读锁的获取次数,是将第一个线程和后续线程分开计数的。

三、锁降级机制

  如果一个线程,先获取到了写锁,然后再去获取读锁,最后释放写锁,写锁能够降级成为读锁,即:一个线程在持有写锁的情况下,将写锁转换为读锁,即允许线程在修改资源之后,在不释放锁的情况下继续读取资源(上述的操作,只针对当前线程)

public class Demo2 {private static final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();private static final ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock();private static final ReentrantReadWriteLock.ReadLock readLock = lock.readLock();public static void main(String[] args) {//先获取到了写锁writeLock.lock();System.out.println(Thread.currentThread().getName() + "获取到写锁");try {//再去获取读锁readLock.lock();System.out.println(Thread.currentThread().getName() + "获取到写锁后,又获取到了读锁");}finally {//最后释放写锁writeLock.unlock();System.out.println(Thread.currentThread().getName() + "释放了写锁");}try{System.out.println("业务代码执行");}finally{readLock.unlock();System.out.println(Thread.currentThread().getName() + "释放了写锁后,释放了读锁");}}
}

在这里插入图片描述  锁降级机制,特别适用于:

  • 数据修改后再查询:当某个线程对共享资源进行了修改(如数据库更新),并且希望读取最新的数据时,直接降级为读锁可以避免不必要的锁切换。
  • 避免频繁锁获取:在某些高并发场景中,多个线程需要频繁读取共享数据,但在一开始线程会进行写操作,降级为读锁可以避免重新获取写锁并减少锁的开销。

  为什么要在获取写锁和释放写锁之间,去获取读锁呢?主要是为了保证数据的一致性。避免先释放写锁-再获取读锁的过程中,其他线程抢先一步获取到了写锁修改了数据。获取读锁-释放写锁,那么其他线程想要获取写锁修改数据,因为读-写锁之间的互斥,所以其他线程将被阻塞。

  读锁尝试获取锁的代码中,下面的片段就是锁降级机制的体现,即已经有了写锁,并且当前线程是该写锁的持有者,那么可以继续获取读锁,不会返回-1失败:

  			//已经有了写锁,并且写锁的拥有者不是当前线程if (exclusiveCount(c) != 0 &&getExclusiveOwnerThread() != current)//当前有写锁且不是当前线程持有的写锁,返回 -1,表示获取读锁失败。return -1;

四、写锁

  在源码的层面,通过AQS的acquire方法,保证不同线程间写锁-写锁,读锁-写锁之间的互斥性。
在这里插入图片描述  案例代码:

public class Demo3 {private static final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();private static final ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock();private static final ReentrantReadWriteLock.ReadLock readLock = lock.readLock();public static void main(String[] args) {writeLock.lock();System.out.println(Thread.currentThread().getName() + "获取到了写锁");try {writeLock.lock();System.out.println(Thread.currentThread().getName() + "再次获取到了写锁");try {System.out.println("....");}finally {writeLock.unlock();System.out.println(Thread.currentThread().getName() + "再次释放了写锁");}}finally {writeLock.unlock();System.out.println(Thread.currentThread().getName() + "释放了写锁");}}
}

在这里插入图片描述同一线程间写锁间(写锁和读锁间)不互斥

public class Demo3 {private static final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();private static final ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock();private static final ReentrantReadWriteLock.ReadLock readLock = lock.readLock();public static void main(String[] args) throws InterruptedException {writeLock.lock();System.out.println(Thread.currentThread().getName() + "获取到了写锁");Thread.sleep(3000);try {new Thread(() -> {writeLock.lock();System.out.println(Thread.currentThread().getName() + "再次获取到了写锁");try {System.out.println("....");}finally {writeLock.unlock();System.out.println(Thread.currentThread().getName() + "再次释放了写锁");}}).start();}finally {writeLock.unlock();System.out.println(Thread.currentThread().getName() + "释放了写锁");}}
}

在这里插入图片描述不同线程间的写锁间互斥(写锁和读锁间也互斥)

    protected final boolean tryAcquire(int acquires) {/** Walkthrough:* 1. If read count nonzero or write count nonzero*    and owner is a different thread, fail.* 2. If count would saturate, fail. (This can only*    happen if count is already nonzero.)* 3. Otherwise, this thread is eligible for lock if*    it is either a reentrant acquire or*    queue policy allows it. If so, update state*    and set owner.*///获取当前线程Thread current = Thread.currentThread();//获取状态int c = getState();//提取当前状态 c 中的写锁计数int w = exclusiveCount(c);//状态不为0  意味着锁当前处于非空状态if (c != 0) {// (Note: if c != 0 and w == 0 then shared count != 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;}//状态为0//应该被阻塞 或 CAS 状态 + 重入次数 累加失败if (writerShouldBlock() ||!compareAndSetState(c, c + acquires))//返回加锁失败return false;//设置当前线程为锁的持有者setExclusiveOwnerThread(current);return true;}

总结

  线程可以获取到读锁的条件:

  • 没有其他线程获取到写锁。
  • 有写锁,但是是当前线程持有(锁降级机制)

  线程可以获取到写锁的条件:

  • 没有其他线程获取到读锁。
  • 没有其他线程获取到写锁。

  对于同一个线程而言:读线程获取读锁后,能够再次获取读锁(不能再获取到写锁,即不支持锁升级)。写线程在获取写锁之后能够再次获取写锁,同时也可以获取读锁。


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

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

相关文章

51单片机开发:温度传感器

温度传感器DS18B20&#xff1a; 初始化时序图如下图所示&#xff1a; u8 ds18b20_init(void){ds18b20_reset();return ds18b20_check(); }void ds18b20_reset(void){DS18B20_PORT 0;delay_10us(75);DS18B20_PORT 1;delay_10us(2); }u8 ds18b20_check(void){u8 time_temp0;wh…

vue2项目(一)

项目介绍 电商前台项目 技术架构&#xff1a;vuewebpackvuexvue-routeraxiosless.. 封装通用组件登录注册token购物车支付项目性能优化 一、项目初始化 使用vue create projrct_vue2在命令行窗口创建项目 1.1、脚手架目录介绍 ├── node_modules:放置项目的依赖 ├──…

labelme_json_to_dataset ValueError: path is on mount ‘D:‘,start on C

这是你的labelme运行时label照片的盘和保存目的地址的盘不同都值得报错 labelme_json_to_dataset ValueError: path is on mount D:,start on C 只需要放一个盘但可以不放一个目录

物联网 STM32【源代码形式-使用以太网】连接OneNet IOT从云产品开发到底层MQTT实现,APP控制 【保姆级零基础搭建】

物联网&#xff08;IoT&#xff09;‌是指通过各种信息传感器、射频识别技术、全球定位系统、红外感应器等装置与技术&#xff0c;实时采集并连接任何需要监控、连接、互动的物体或过程&#xff0c;实现对物品和过程的智能化感知、识别和管理。物联网的核心功能包括数据采集与监…

无心剑七绝《深度求索》

七绝深度求索 深研妙理定乾坤 度世玄机启智门 求路千难兼万险 索萦华夏自为尊 2025年2月1日 平水韵十三元平韵 无心剑七绝《深度求索》以平水韵十三元平韵写成&#xff0c;意境深远&#xff0c;气势磅礴。诗中“深研妙理定乾坤”开篇点题&#xff0c;展现出对深奥道理的钻研与探…

Hot100之普通数组

53最大子数组和 题目 思路解析 我们用一个dp数组来收集我们从左往右&#xff0c;加起来的最大的和 也就是我们的节点不是负数&#xff0c;那我们直接收集就好了 如果是负数&#xff0c;我们就用Max&#xff08;&#xff09;比较是这个节点大还是当前节点大&#xff08;这个情…

如何利用天赋实现最大化的价值输出-补

原文&#xff1a; https://blog.csdn.net/ZhangRelay/article/details/145408621 ​​​​​​如何利用天赋实现最大化的价值输出-CSDN博客 如何利用天赋实现最大化的价值输出-CSDN博客 引用视频差异 第一段视频目标明确&#xff0c;建议也非常明确。 录制视频的人是主动性…

新能源算力战争:为什么AI大模型需要绿色数据中心?

新能源算力战争:为什么AI大模型需要绿色数据中心? 近年来,人工智能(AI)大模型的爆发式增长正在重塑全球科技产业的格局。以GPT-4、Gemini、Llama等为代表的千亿参数级模型,不仅需要海量数据训练,更依赖庞大的算力支撑。然而,这种算力的背后隐藏着一个日益严峻的挑战——…

Spring Boot 中的事件发布与监听:深入理解 ApplicationEventPublisher(附Demo)

目录 前言1. 基本知识2. Demo3. 实战代码 前言 &#x1f91f; 找工作&#xff0c;来万码优才&#xff1a;&#x1f449; #小程序://万码优才/r6rqmzDaXpYkJZF 基本的Java知识推荐阅读&#xff1a; java框架 零基础从入门到精通的学习路线 附开源项目面经等&#xff08;超全&am…

unity学习24:场景scene相关生成,加载,卸载,加载进度,异步加载场景等

目录 1 场景数量 SceneManager.sceneCount 2 直接代码生成新场景 SceneManager.CreateScene 3 场景的加载 3.1 用代码加载场景&#xff0c;仍然build setting里先加入配置 3.2 卸载场景 SceneManager.UnloadSceneAsync(); 3.3 同步加载场景 SceneManager.LoadScene 3.3.…

【Android】布局文件layout.xml文件使用控件属性android:layout_weight使布局较为美观,以RadioButton为例

目录 说明举例 说明 简单来说&#xff0c;android:layout_weight为当前控件按比例分配剩余空间。且单个控件该属性的具体数值不重要&#xff0c;而是多个控件的属性值之比发挥作用&#xff0c;例如有2个控件&#xff0c;各自的android:layout_weight的值设为0.5和0.5&#xff0…

hot100_21. 合并两个有序链表

将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。 示例 1&#xff1a; 输入&#xff1a;l1 [1,2,4], l2 [1,3,4] 输出&#xff1a;[1,1,2,3,4,4] 示例 2&#xff1a; 输入&#xff1a;l1 [], l2 [] 输出&#xff1a;[…

4 [危机13小时追踪一场GitHub投毒事件]

事件概要 自北京时间 2024.12.4 晚间6点起&#xff0c; GitHub 上不断出现“幽灵仓库”&#xff0c;仓库中没有任何代码&#xff0c;只有诱导性的病毒文件。当天&#xff0c;他们成为了 GitHub 上 star 增速最快的仓库。超过 180 个虚假僵尸账户正在传播病毒&#xff0c;等待不…

Spring Boot项目中解决跨域问题(四种方式)

目录 一&#xff0c;跨域产生的原因二&#xff0c;什么情况下算跨域三&#xff0c;实际演示四&#xff0c;解决跨域的方法 1&#xff0c;CrossOrigin注解2&#xff0c;添加全局过滤器3&#xff0c;实现WebMvcConfigurer4&#xff0c;Nginx解决跨域5&#xff0c;注意 开发项目…

浅析DNS污染及防范

DNS污染&#xff08;DNS Cache Poisoning&#xff09;是一种网络攻击手段&#xff0c;通过篡改DNS服务器的缓存数据&#xff0c;将域名解析结果指向错误的IP地址&#xff0c;从而误导用户访问恶意网站或无法访问目标网站。这种攻击利用了DNS协议的特性&#xff0c;例如“只认第…

五. Redis 配置内容(详细配置说明)

五. Redis 配置内容(详细配置说明) 文章目录 五. Redis 配置内容(详细配置说明)1. Units 单位配置2. INCLUDES (包含)配置3. NETWORK (网络)配置3.1 bind(配置访问内容)3.2 protected-mode (保护模式)3.3 port(端口)配置3.4 timeout(客户端超时时间)配置3.5 tcp-keepalive()配置…

单细胞分析基础-第一节 数据质控、降维聚类

scRNA_pipeline\1.Seurat 生物技能树 可进官网查询 添加链接描述 分析流程 准备:R包安装 options("repos"="https://mirrors.ustc.edu.cn/CRAN/") if(!require("BiocManager")) install.packages("BiocManager",update = F,ask =…

Qt常用控件 输入类控件

文章目录 1.QLineEdit1.1 常用属性1.2 常用信号1.3 例子1&#xff0c;录入用户信息1.4 例子2&#xff0c;正则验证手机号1.5 例子3&#xff0c;验证输入的密码1.6 例子4&#xff0c;显示密码 2. QTextEdit2.1 常用属性2.2 常用信号2.3 例子1&#xff0c;获取输入框的内容2.4 例…

大模型培训讲师老师叶梓分享:DeepSeek多模态大模型janus初探

以下视频内容为叶梓分享DeepSeek多模态大模型janus的部署&#xff0c;并验证其实际效果&#xff0c;包括图生文和文生图两部分。 叶梓老师人工智能培训分享DeepSeek多模态大模型janus初探 DeepSeek 的多模态大模型 Janus 是一款强大的 AI 模型&#xff0c;专注于图像和文本的多…

Linux系统上安装与配置 MySQL( CentOS 7 )

目录 1. 下载并安装 MySQL 官方 Yum Repository 2. 启动 MySQL 并查看运行状态 3. 找到 root 用户的初始密码 4. 修改 root 用户密码 5. 设置允许远程登录 6. 在云服务器配置 MySQL 端口 7. 关闭防火墙 8. 解决密码错误的问题 前言 在 Linux 服务器上安装并配置 MySQL …