当前位置: 首页 > news >正文

ReetrantReadWriteLock底层原理

文章目录

    • 一、读写锁介绍
    • 二、ReentrantReadWriteLock底层原理
      • 1. 读写锁的设计

一、读写锁介绍

现实中有这样一种场景:对共享资源有读和写的操作,且写操作没有读操作那么频繁(读多写少)。在没有写操作的时候,多个线程同时读一个资源没有任何问题,所以应该允许多个线程同时读取共享资源(读读可以并发);但是如果一个线程想去写这些共享资源,就不应该允许其他线程对该资源进行读和写操作了(读写,写读,写写互斥)。在读多于写的情况下,读写锁能够提供比排它锁更好的并发性和吞吐量。

针对这种场景,JAVA的并发包提供了读写锁ReentrantReadWriteLock,它内部,维护了 一对相关的锁,一个用于只读操作,称为读锁;一个用于写入操作,称为写锁。

线程进入读锁的前提条件:

  • 没有其它线程的写锁
  • 没有写请求或者有写请求,但是调用线程和持有锁的线程是同一个

线程进入写锁的前提条件:

  • 没有其他线程的读锁
  • 没有其他线程的写锁

读写锁有以下三个重要的特性:

  • 公平选择性:支持非公平和公平的锁获取方式,吞吐量还是非公平优于公平
  • 可重入:读锁和写锁都支持线程重入,以读写线程为例,读锁获取读锁后,能够再次获取读锁,写线程在获取写锁之后能够再次获取写锁,同时也可以获取读锁。
  • 锁降级:遵循获取写锁,再获取读锁最后释放写锁的次序,写锁能够降级为读锁。

二、ReentrantReadWriteLock底层原理

看源码需要了解三个核心问题

  1. 读写锁是怎样实现分别记录读写状态的?
  2. 写锁时怎么获取和释放的?
  3. 读锁时怎么获取和释放的?

1. 读写锁的设计

在这里插入图片描述
首先看它的类信息:

public class ReentrantReadWriteLockimplements ReadWriteLock, java.io.Serializable {}

可以发现该类实现了ReadWriteLock这个接口

public interface ReadWriteLock {Lock readLock();Lock writeLock();
}

该接口就实现了读锁和写锁的规范,以下就是相关的类图

在这里插入图片描述
下面看看ReentrantReadWriteLock的读写锁的实现逻辑,首先看写锁:

public static class ReadLock implements Lock, java.io.Serializable {private static final long serialVersionUID = -5992448646407690164L;private final Sync sync;protected ReadLock(ReentrantReadWriteLock lock) {sync = lock.sync;}public void lock() {sync.acquireShared(1);}public void lockInterruptibly() throws InterruptedException {sync.acquireSharedInterruptibly(1);}public boolean tryLock() {return sync.tryReadLock();}public boolean tryLock(long timeout, TimeUnit unit)throws InterruptedException {return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));}public void unlock() {sync.releaseShared(1);}public Condition newCondition() {throw new UnsupportedOperationException();}public String toString() {int r = sync.getReadLockCount();return super.toString() +"[Read locks = " + r + "]";}}
  • ReadLock:是一个ReetrantReadWriteLock的静态内部类
  • Sync:和ReentrantLock一样,Sync也是ReetrantReadWriteLock的一个静态内部抽象类,它继承了AbstractQueuedSynchronizer,实现了AQS的逻辑,然后它会有两种实现,分别是FairSync公平锁,和NonfairSync非公平锁
  • lock:可以发现加读锁加的是AQS的共享锁
  • tryLock:尝试获取读锁

然后看看写锁是怎么实现的

 public static class WriteLock implements Lock, java.io.Serializable {private static final long serialVersionUID = -4992448646407690164L;private final Sync sync;protected WriteLock(ReentrantReadWriteLock lock) {sync = lock.sync;}public void lock() {sync.acquire(1);}public void lockInterruptibly() throws InterruptedException {sync.acquireInterruptibly(1);}public boolean tryLock( ) {return sync.tryWriteLock();}public boolean tryLock(long timeout, TimeUnit unit)throws InterruptedException {return sync.tryAcquireNanos(1, unit.toNanos(timeout));}public void unlock() {sync.release(1);}public Condition newCondition() {return sync.newCondition();}public String toString() {Thread o = sync.getOwner();return super.toString() + ((o == null) ?"[Unlocked]" :"[Locked by thread " + o.getName() + "]");}public boolean isHeldByCurrentThread() {return sync.isHeldExclusively();}public int getHoldCount() {return sync.getWriteHoldCount();}}

下面我们需要思考一个核心问题,读写锁的状态是怎么用AQS底层的state状态来维护的。其实这里的核心问题就是,如何用一个变量维护多种状态。在 ReentrantLock 中,使用 Sync ( 实际是 AQS )的 int 类型的 state 来表示同步状态,表示锁被一个线程重复获取的次数。但是,读写锁 ReentrantReadWriteLock 内部维护着一对读写锁,如果要用一个变量维护多种状态,需要采用“按位切割使用”的方式来维护这个变量,将其切分为两部分:高16为表示读,低16为表示写。分割之后,读写锁是如何迅速确定读锁和写锁的状态呢? 其实底层是通过位运算来实现的。假如当前同步状态为S, 那么写状态,等于 S & 0x0000FFFF(将高 16 位全部抹去)。 当写状态加1,等于S+1。读状态,等于 S >>> 16 (无符号补 0 右移 16 位)。当读状态加1,等于 S+(1<<16),也就是S+0x00010000。根据状态的划分能得出一个推论:S不等于0时,当写状态(S&0x0000FFFF)等于0时,则读状态(S>>>16)大于0,即读锁已被获取。
在这里插入图片描述
源码如下:

//该部分在Sync类中static final int SHARED_SHIFT   = 16;static final int SHARED_UNIT    = (1 << SHARED_SHIFT);static final int MAX_COUNT      = (1 << SHARED_SHIFT) - 1;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  */static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }
  • exclusiveCount:获得持有写状态的锁的次数
  • sharedCount:获得持有读状态的锁的线程数量,不同于写锁,读锁利用同时被多个线程持有,而每个线程持有的读锁支持重入的特性,所以需要每个线程持有的读锁数量单独计数,这就需要HoldCounter计数器。

HoldCounter计数器
读锁的内在机制其实就是一个共享锁。一次共享锁的操作就相当于对HoldCounter 计数器的操作。获取共享锁,则该计数器 + 1,释放共享锁,该计数器 - 1。只有当线程获取共享锁后才能对共享锁进行释放、重入操作。

static final class HoldCounter {int count = 0;// Use id, not reference, to avoid garbage retentionfinal long tid = getThreadId(Thread.currentThread());}static final class ThreadLocalHoldCounterextends ThreadLocal<HoldCounter> {public HoldCounter initialValue() {return new HoldCounter();}}

写锁的获取
写锁是一个支持重进入的排它锁。如果当前线程已经获取了写锁,则增加写状态。如果当前线程在获取写锁时,读锁已经被获取(读状态不为0)或者该线程不是已经获取写锁的线程, 则当前线程进入等待状态。

protected final boolean tryAcquire(int acquires) {
//获取当前线程Thread current = Thread.currentThread();//获取当前state的值int c = getState();//获取写锁的重入次数int w = exclusiveCount(c);//state!=0则表示当前有写锁或读锁if (c != 0) {//判断是否是重入if (w == 0 || current != getExclusiveOwnerThread())return false;if (w + exclusiveCount(acquires) > MAX_COUNT)throw new Error("Maximum lock count exceeded");//获取重入锁setState(c + acquires);return true;}// writerShouldBlock有公平与非公平的实现, 非公平返回false,会尝试通过cas加锁,c==0 写锁未被任何线程获取,当前线程是否阻塞或者cas尝试获取锁if (writerShouldBlock() ||!compareAndSetState(c, c + acquires))return false;//设置锁由当前线程独占setExclusiveOwnerThread(current);return true;}

上面简单的代码就实现了下面的逻辑:

  • 读写互斥
  • 写写互斥
  • 写锁支持同一个线程重入
  • writeShouldBlock写锁是否阻塞实现取决公平与非公平的策略
    在这里插入图片描述

写锁的释放

     protected final boolean tryRelease(int releases) {if (!isHeldExclusively())throw new IllegalMonitorStateException();//设置状态int nextc = getState() - releases;boolean free = exclusiveCount(nextc) == 0;//如果state为0,表示释放写锁if (free)setExclusiveOwnerThread(null);setState(nextc);return free;}

在这里插入图片描述
读锁的获取

protected final int tryAcquireShared(int unused) {//获取当前线程Thread current = Thread.currentThread();//获取stateint c = getState();//exclusiveCount(c) != 0 判断是否有写锁//getExclusiveOwnerThread() != current),判断当前线程是否是写锁的持有者if (exclusiveCount(c) != 0 &&getExclusiveOwnerThread() != current)return -1;int r = sharedCount(c);if (!readerShouldBlock() &&r < MAX_COUNT &&//cas加读锁compareAndSetState(c, c + SHARED_UNIT)) {//r==0表示第一次获取读锁if (r == 0) {//设置第一个读为当前线程firstReader = current;//设置当前读锁的重入次数firstReaderHoldCount = 1;} else if (firstReader == current) {          //第一个读的重入firstReaderHoldCount++;} else {//若不是第一个读,则用HoldCounter记录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);//第一次读锁获取失败,再次尝试(fullTryAcquireShared)}

上面代码的逻辑就实现了:

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

在这里插入图片描述
读锁的释放

protected final boolean tryReleaseShared(int unused) {Thread current = Thread.currentThread();if (firstReader == current) {// assert firstReaderHoldCount > 0;if (firstReaderHoldCount == 1)firstReader = null;elsefirstReaderHoldCount--;} else {HoldCounter rh = cachedHoldCounter;if (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;}for (;;) {int c = getState();int nextc = c - SHARED_UNIT;if (compareAndSetState(c, nextc))return nextc == 0;}}

在这里插入图片描述

相关文章:

ReetrantReadWriteLock底层原理

文章目录 一、读写锁介绍二、ReentrantReadWriteLock底层原理1. 读写锁的设计 一、读写锁介绍 现实中有这样一种场景:对共享资源有读和写的操作&#xff0c;且写操作没有读操作那么频繁(读多写少)。在没有写操作的时候&#xff0c;多个线程同时读一个资源没有任何问题&#xf…...

LeetCode力扣每日一题(Java):35、搜索插入位置

一、题目 二、解题思路 1、我的思路&#xff08;又称&#xff1a;论API的重要性&#xff09; 读完题目之后&#xff0c;我心想这题目怎么看着这么眼熟&#xff1f;好像我之前学过的一个API呀&#xff01; 于是我回去翻了翻我之前写的博客&#xff1a;小白备战蓝桥杯&#xf…...

Unity中结构体定义的成员如何显示在窗口中

在Unity中&#xff0c;有时候我们在处理数据的时候会用到结构体定义一些Unity组件相关的数据成员&#xff0c;并且需要在编辑器中拉取对象赋值。比如&#xff1a; using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.UI;publ…...

Python3开发环境的搭建

1&#xff0c;电脑操作系统的确认 我的是win10、64位的&#xff0c;你们的操作系统可自寻得。 2&#xff0c;Python安装包的下载 &#xff08;1&#xff09;浏览器种输入网址&#xff1a;https://www.python.org 选择对应的系统&#xff08;我的是win10/64位) &#xf…...

Leetcode 2957. Remove Adjacent Almost-Equal Characters

Leetcode 2957. Remove Adjacent Almost-Equal Characters 1. 解题思路2. 代码实现 题目链接&#xff1a;2957. Remove Adjacent Almost-Equal Characters 1. 解题思路 这一题其实不是很想放上来的&#xff0c;因为其实真的很简单&#xff0c;但是我惊讶地发现当前提交的算法…...

透析跳跃游戏

关卡名 理解与贪心有关的高频问题 我会了✔️ 内容 1.理解跳跃游戏问题如何判断是否能到达终点 ✔️ 2.如果能到终点&#xff0c;如何确定最少跳跃次数 ✔️ 1. 跳跃游戏 leetCode 55 给定一个非负整数数组&#xff0c;你最初位于数组的第一个位置。数组中的每个元素代表…...

贵州开放大学形成性考核 平时作业 参考试题

试卷代号&#xff1a;1310 古代汉语专题 参考试题&#xff08;开卷&#xff09; 一、单项选择题&#xff08;每题3分&#xff0c;共10题30分&#xff09; 1.“六书”的具体类别名称始见于( )。 A.《汉书艺文志》 B.《说文解字》 C.《周礼》 2.汉字的…...

Leetcode 2962. Count Subarrays Where Max Element Appears at Least K Times

Leetcode 2962. Count Subarrays Where Max Element Appears at Least K Times 1. 解题思路2. 代码实现 题目链接&#xff1a;2962. Count Subarrays Where Max Element Appears at Least K Times 1. 解题思路 这一题思路上同样很直接&#xff0c;就是找到最大的元素所在的全…...

Mybatis XML 配置文件

我们刚开始就有说Mybatis 的开发有两种方式: 1.注释 2.XML 注解和 XML 的方式是可以共存的 我们前面说的都是注释的方式,接下来是XML方式 XML的方式分为三步 : 1.配置数据库(配在 application.yml 里面) 这个跟注释的配置是一样的,username应该都是一样的,password记得写…...

CCF计算机软件能力认证202309-1坐标变换(其一)(C语言)

ccf-csp计算机软件能力认证202309-1坐标变换&#xff08;其一&#xff09;(C语言版) 题目内容&#xff1a; 问题描述 输入格式 输出格式 样例输入 3 2 10 10 0 0 10 -20 1 -1 0 0样例输出 21 -11 20 -10样例解释 评测用例规模与约定 解题思路 1.第一步分析问题&…...

k8s 如何部署Mysql(史上最权威教程)?

Kuboard K8s 部署Mysql5.7-8.x版本 部署Mysql5.7 在 Kuboard 界面进入名称空间 &#xff08;自己的命令空间&#xff09;&#xff0c;点击 创建工作负载 按钮&#xff0c;并填写表单&#xff0c;如下图所示&#xff1a; 字段名称填写内容工作负载类型有状态副本集&#xff0…...

红队攻防实战之Redis-RCE集锦

心若有所向往&#xff0c;何惧道阻且长 Redis写入SSH公钥实现RCE 之前进行端口扫描时发现该机器开着6379&#xff0c;尝试Redis弱口令或未授权访问 尝试进行连接Redis&#xff0c;连接成功&#xff0c;存在未授权访问 尝试写入SSH公钥 设置redis的备份路径 设置保存文件名 …...

六级翻译之印章

好像大房子挺难得 三段式 1Since ancient from now&#xff0c;seals have been a symbol of power and certerfiction of identity.seals not only practical but also is a form of art.Seal is an ancient art combining with manafutuer of crafting and desgin of…...

PHP数据库操作实例 - 学生信息管理

文章目录 一、启动Apache与MySQL服务二、创建数据库与表(一)创建数据库(二)创建表并插入记录三、项目实现步骤(一)创建项目(二)创建学生类(二)获取数据库连接(三)学生数据访问对象(四)创建功能页面1、按学号查询学生页面2、处理按学号查找学生记录页面3、插入学生…...

企业架构LB-服务器的负载均衡之LVS实现

企业架构LB-服务器的负载均衡之LVS实现 学习目标和内容 1、能够了解LVS的基本工作方式 2、能够安装配置LVS实现负载均衡 3、能够了解LVS-NAT的配置方式 4、能够了解LVS-DR的配置方式 #一、LVS介绍和安装 LVS&#xff08;Linux Virtual Server&#xff09;即Linux虚拟服务器&…...

Java程序设计基础 - 课程概述

文章目录 一、程序员最具共性的心理特征二、Java开发工程师的岗位要求(一)素质和职业道德需求(二)岗位能力需求统计三、针对Java工程师岗位需求的课程目标(一)熟练掌握Java编程语言,掌握编程技能(二)精通使用集成开发工具Eclipse或IntelliJ IDEA(三)需要将“用户体验…...

基于SpringBoot+Vue前后端分离的商城管理系统(Java毕业设计)

大家好&#xff0c;我是DeBug&#xff0c;很高兴你能来阅读&#xff01;作为一名热爱编程的程序员&#xff0c;我希望通过这些教学笔记与大家分享我的编程经验和知识。在这里&#xff0c;我将会结合实际项目经验&#xff0c;分享编程技巧、最佳实践以及解决问题的方法。无论你是…...

vue3中实现el-tree通过ctrl或shift批量选择节点并高亮展示

一、看效果&#xff1a; 按住ctrl键实现单个多选 按住shift实现区间范围多选 二、代码&#xff1a; vue页面 <template><el-treeclass"w100%":data"$.treeData"ref"treeTab…...

HarmonyOS 振动效果开发指导

Vibrator 开发概述 振动器模块服务最大化开放硬工最新马达器件能力&#xff0c;通过拓展原生马达服务实现振动与交互融合设计&#xff0c;打造细腻精致的一体化振动体验和差异化体验&#xff0c;提升用户交互效率和易用性、提升用户体验、增强品牌竞争力。 运作机制 Vibrato…...

【ACM独立出版、确定的ISBN号】第三届密码学、网络安全和通信技术国际会议(CNSCT 2024)

第三届密码学、网络安全和通信技术国际会议&#xff08;CNSCT 2024&#xff09; 2024 3rd International Conference on Cryptography, Network Security and Communication Technology 随着互联网和网络应用的不断发展&#xff0c;网络安全在计算机科学中的地位越来越重要&…...

前端倒计时误差!

提示:记录工作中遇到的需求及解决办法 文章目录 前言一、误差从何而来?二、五大解决方案1. 动态校准法(基础版)2. Web Worker 计时3. 服务器时间同步4. Performance API 高精度计时5. 页面可见性API优化三、生产环境最佳实践四、终极解决方案架构前言 前几天听说公司某个项…...

安宝特方案丨XRSOP人员作业标准化管理平台:AR智慧点检验收套件

在选煤厂、化工厂、钢铁厂等过程生产型企业&#xff0c;其生产设备的运行效率和非计划停机对工业制造效益有较大影响。 随着企业自动化和智能化建设的推进&#xff0c;需提前预防假检、错检、漏检&#xff0c;推动智慧生产运维系统数据的流动和现场赋能应用。同时&#xff0c;…...

聊聊 Pulsar:Producer 源码解析

一、前言 Apache Pulsar 是一个企业级的开源分布式消息传递平台&#xff0c;以其高性能、可扩展性和存储计算分离架构在消息队列和流处理领域独树一帜。在 Pulsar 的核心架构中&#xff0c;Producer&#xff08;生产者&#xff09; 是连接客户端应用与消息队列的第一步。生产者…...

蓝桥杯 2024 15届国赛 A组 儿童节快乐

P10576 [蓝桥杯 2024 国 A] 儿童节快乐 题目描述 五彩斑斓的气球在蓝天下悠然飘荡&#xff0c;轻快的音乐在耳边持续回荡&#xff0c;小朋友们手牵着手一同畅快欢笑。在这样一片安乐祥和的氛围下&#xff0c;六一来了。 今天是六一儿童节&#xff0c;小蓝老师为了让大家在节…...

如何在网页里填写 PDF 表格?

有时候&#xff0c;你可能希望用户能在你的网站上填写 PDF 表单。然而&#xff0c;这件事并不简单&#xff0c;因为 PDF 并不是一种原生的网页格式。虽然浏览器可以显示 PDF 文件&#xff0c;但原生并不支持编辑或填写它们。更糟的是&#xff0c;如果你想收集表单数据&#xff…...

Git常用命令完全指南:从入门到精通

Git常用命令完全指南&#xff1a;从入门到精通 一、基础配置命令 1. 用户信息配置 # 设置全局用户名 git config --global user.name "你的名字"# 设置全局邮箱 git config --global user.email "你的邮箱example.com"# 查看所有配置 git config --list…...

Caliper 负载(Workload)详细解析

Caliper 负载(Workload)详细解析 负载(Workload)是 Caliper 性能测试的核心部分,它定义了测试期间要执行的具体合约调用行为和交易模式。下面我将全面深入地讲解负载的各个方面。 一、负载模块基本结构 一个典型的负载模块(如 workload.js)包含以下基本结构: use strict;/…...

pikachu靶场通关笔记19 SQL注入02-字符型注入(GET)

目录 一、SQL注入 二、字符型SQL注入 三、字符型注入与数字型注入 四、源码分析 五、渗透实战 1、渗透准备 2、SQL注入探测 &#xff08;1&#xff09;输入单引号 &#xff08;2&#xff09;万能注入语句 3、获取回显列orderby 4、获取数据库名database 5、获取表名…...

在树莓派上添加音频输入设备的几种方法

在树莓派上添加音频输入设备可以通过以下步骤完成&#xff0c;具体方法取决于设备类型&#xff08;如USB麦克风、3.5mm接口麦克风或HDMI音频输入&#xff09;。以下是详细指南&#xff1a; 1. 连接音频输入设备 USB麦克风/声卡&#xff1a;直接插入树莓派的USB接口。3.5mm麦克…...

pycharm 设置环境出错

pycharm 设置环境出错 pycharm 新建项目&#xff0c;设置虚拟环境&#xff0c;出错 pycharm 出错 Cannot open Local Failed to start [powershell.exe, -NoExit, -ExecutionPolicy, Bypass, -File, C:\Program Files\JetBrains\PyCharm 2024.1.3\plugins\terminal\shell-int…...