ReentrantReadWriteLock学习

简介

ReentrantReadWriteLock 是 Java 并发包(java.util.concurrent.locks)中的一个类,它实现了一个可重入的读写锁。读写锁允许多个线程同时读取共享资源,但在写入共享资源时只允许一个线程进行。这种锁机制特别适用于读多写少的场景,可以显著提高并发性能。

ReentrantReadWriteLock 包含两个锁:一个用于读操作(ReadLock),另一个用于写操作(WriteLock)。这两个锁是互斥的,即一个线程拥有写锁时,其他线程无法获取读锁或写锁;一个线程拥有读锁时,其他线程无法获取写锁,但可以获取读锁(这取决于锁的公平性和当前读锁的持有数量)。

以下是 ReentrantReadWriteLock 的一些关键特性和用法:

  • 可重入性:持有锁的线程可以再次获取同一个锁,而不会被阻塞。
  • 公平性:可以通过构造函数指定锁是否公平。公平锁会按照线程请求锁的顺序来授予锁,而非公平锁则不保证这种顺序。
  • 读锁和写锁:通过 readLock() 和 writeLock() 方法获取读锁和写锁。
  • 锁升级:一个线程可以先获取读锁,然后升级为写锁,但不允许先获取写锁再降级为读锁。

源码

public class ReentrantReadWriteLockimplements ReadWriteLock, java.io.Serializable {private static final long serialVersionUID = -6992448646407690164L;/** 读锁*/private final ReentrantReadWriteLock.ReadLock readerLock;/**写锁 */private final ReentrantReadWriteLock.WriteLock writerLock;/** Performs all synchronization mechanics */final Sync sync;/*** 默认构造是非公平锁*/public ReentrantReadWriteLock() {this(false);}
}

HoldCounter

// 计数器
//HoldCounter主要有两个属性,count和tid,其中count表示某个读线程重入的次数,tid表示该线程的tid字段的值,该字段可以用来唯一标识一个线程。
static final class HoldCounter {// 计数int count = 0;// Use id, not reference, to avoid garbage retention// 获取当前线程的TID属性的值final long tid = getThreadId(Thread.currentThread());
}

ThreadLocalHoldCounter

//ThreadLocalHoldCounter重写了ThreadLocal的initialValue方法,ThreadLocal类可以将线程与对象相关联。在没有进行set的情况下,get到的均是initialValue方法里面生成的那个HolderCounter对象。
static final class ThreadLocalHoldCounterextends ThreadLocal<HoldCounter> {public HoldCounter initialValue() {return new HoldCounter();}
}

Sync

abstract static class Sync extends AbstractQueuedSynchronizer {// 版本序列号private static final long serialVersionUID = 6317671515068378041L;        // 高16位为读锁,低16位为写锁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;// 本地线程计数器private transient ThreadLocalHoldCounter readHolds;// 缓存的计数器private transient HoldCounter cachedHoldCounter;// 第一个读线程private transient Thread firstReader = null;// 第一个读线程的计数private transient int firstReaderHoldCount;
}

tryAcquire()

protected final boolean tryAcquire(int acquires) {//当前线程Thread current = Thread.currentThread();//获取状态int c = getState();//写线程数量(即获取独占锁的重入数)int w = exclusiveCount(c);//当前同步状态state != 0,说明已经有其他线程获取了读锁或写锁if (c != 0) {// 当前state不为0,此时:如果写锁状态为0说明读锁此时被占用返回false;// 如果写锁状态不为0且写锁没有被当前线程持有返回falseif (w == 0 || current != getExclusiveOwnerThread())return false;//判断同一线程获取写锁是否超过最大次数(65535),支持可重入if (w + exclusiveCount(acquires) > MAX_COUNT)throw new Error("Maximum lock count exceeded");//更新状态//此时当前线程已持有写锁,现在是重入,所以只需要修改锁的数量即可。setState(c + acquires);return true;}//到这里说明此时c=0,读锁和写锁都没有被获取//writerShouldBlock表示是否阻塞if (writerShouldBlock() ||!compareAndSetState(c, c + acquires))return false;//设置锁为当前线程所有setExclusiveOwnerThread(current);return true;
}

(1)首先获取c、w。c表示当前锁状态;w表示写线程数量。然后判断同步状态state是否为0。如果state!=0,说明已经有其他线程获取了读锁或写锁,执行(2);否则执行(5)。

(2)如果锁状态不为零(c != 0),而写锁的状态为0(w = 0),说明读锁此时被其他线程占用,所以当前线程不能获取写锁,自然返回false。或者锁状态不为零,而写锁的状态也不为0,但是获取写锁的线程不是当前线程,则当前线程也不能获取写锁。

(3)判断当前线程获取写锁是否超过最大次数,若超过,抛异常,反之更新同步状态(此时当前线程已获取写锁,更新是线程安全的),返回true。

(4)如果state为0,此时读锁或写锁都没有被获取,判断是否需要阻塞(公平和非公平方式实现不同),在非公平策略下总是不会被阻塞,在公平策略下会进行判断(判断同步队列中是否有等待时间更长的线程,若存在,则需要被阻塞,否则,无需阻塞),如果不需要阻塞,则CAS更新同步状态,若CAS成功则返回true,失败则说明锁被别的线程抢去了,返回false。如果需要阻塞则也返回false。

(5)成功获取写锁后,将当前线程设置为占有写锁的线程,返回true。

tryRelease()

protected final boolean tryRelease(int releases) {//若锁的持有者不是当前线程,抛出异常if (!isHeldExclusively())throw new IllegalMonitorStateException();//写锁的新线程数int nextc = getState() - releases;//如果独占模式重入数为0了,说明独占模式被释放boolean free = exclusiveCount(nextc) == 0;if (free)//若写锁的新线程数为0,则将锁的持有者设置为nullsetExclusiveOwnerThread(null);//设置写锁的新线程数//不管独占模式是否被释放,更新独占重入数setState(nextc);return free;
}

写锁的释放过程还是相对而言比较简单的:首先查看当前线程是否为写锁的持有者,如果不是抛出异常。然后检查释放后写锁的线程数是否为0,如果为0则表示写锁空闲了,释放锁资源将锁的持有线程设置为null,否则释放仅仅只是一次重入锁而已,并不能将写锁的线程清空。

说明:此方法用于释放写锁资源,首先会判断该线程是否为独占线程,若不为独占线程,则抛出异常,否则,计算释放资源后的写锁的数量,若为0,表示成功释放,资源不将被占用,否则,表示资源还被占用。

tryAcquireShared()

protected final int tryAcquireShared(int unused) {// 获取当前线程Thread current = Thread.currentThread();// 获取状态int c = getState();//如果写锁线程数 != 0 ,且独占锁不是当前线程则返回失败,因为存在锁降级if (exclusiveCount(c) != 0 &&getExclusiveOwnerThread() != current)return -1;// 读锁数量int r = sharedCount(c);/** readerShouldBlock():读锁是否需要等待(公平锁原则)* r < MAX_COUNT:持有线程小于最大数(65535)* compareAndSetState(c, c + SHARED_UNIT):设置读取锁状态*/// 读线程是否应该被阻塞、并且小于最大值、并且比较设置成功if (!readerShouldBlock() &&r < MAX_COUNT &&compareAndSetState(c, c + SHARED_UNIT)) {//r == 0,表示第一个读锁线程,第一个读锁firstRead是不会加入到readHolds中if (r == 0) { // 读锁数量为0// 设置第一个读线程firstReader = current;// 读线程占用的资源数为1firstReaderHoldCount = 1;} else if (firstReader == current) { // 当前线程为第一个读线程,表示第一个读锁线程重入// 占用资源数加1firstReaderHoldCount++;} else { // 读锁数量不为0并且不为当前线程// 获取计数器HoldCounter rh = cachedHoldCounter;// 计数器为空或者计数器的tid不为当前正在运行的线程的tidif (rh == null || rh.tid != getThreadId(current))// 获取当前线程对应的计数器cachedHoldCounter = rh = readHolds.get();else if (rh.count == 0) // 计数为0//加入到readHolds中readHolds.set(rh);//计数+1rh.count++;}return 1;}return fullTryAcquireShared(current);
}

读锁获取锁的过程比写锁稍微复杂些,首先判断写锁是否为0并且当前线程不占有独占锁,直接返回;否则,判断读线程是否需要被阻塞并且读锁数量是否小于最大值并且比较设置状态成功,若当前没有读锁,则设置第一个读线程firstReader和firstReaderHoldCount;若当前线程线程为第一个读线程,则增加firstReaderHoldCount;否则,将设置当前线程对应的HoldCounter对象的值。

tryReleaseShared()

protected final boolean tryReleaseShared(int unused) {// 获取当前线程Thread current = Thread.currentThread();if (firstReader == current) { // 当前线程为第一个读线程// assert firstReaderHoldCount > 0;if (firstReaderHoldCount == 1) // 读线程占用的资源数为1firstReader = null;else // 减少占用的资源firstReaderHoldCount--;} else { // 当前线程不为第一个读线程// 获取缓存的计数器HoldCounter rh = cachedHoldCounter;if (rh == null || rh.tid != getThreadId(current)) // 计数器为空或者计数器的tid不为当前正在运行的线程的tid// 获取当前线程对应的计数器rh = readHolds.get();// 获取计数int count = rh.count;if (count <= 1) { // 计数小于等于1// 移除readHolds.remove();if (count <= 0) // 计数小于等于0,抛出异常throw unmatchedUnlockException();}// 减少计数--rh.count;}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;}
}

此方法表示读锁线程释放锁。首先判断当前线程是否为第一个读线程firstReader,若是,则判断第一个读线程占有的资源数firstReaderHoldCount是否为1,若是,则设置第一个读线程firstReader为空,否则,将第一个读线程占有的资源数firstReaderHoldCount减1;若当前线程不是第一个读线程,那么首先会获取缓存计数器(上一个读锁线程对应的计数器 ),若计数器为空或者tid不等于当前线程的tid值,则获取当前线程的计数器,如果计数器的计数count小于等于1,则移除当前线程对应的计数器,如果计数器的计数count小于等于0,则抛出异常,之后再减少计数即可。无论何种情况,都会进入无限循环,该循环可以确保成功设置状态state。

一个线程要想同时持有写锁和读锁,必须先获取写锁再获取读锁;写锁可以“降级”为读锁;读锁不能“升级”为写锁。

示例

import java.util.concurrent.locks.ReentrantReadWriteLock;  public class SharedResource {  private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();  private int data;  public void read() {  rwLock.readLock().lock(); // 获取读锁  try {  // 执行读操作,如读取 data 变量  System.out.println("Read: " + data);  } finally {  rwLock.readLock().unlock(); // 释放读锁  }  }  public void write(int newData) {  rwLock.writeLock().lock(); // 获取写锁  try {  // 执行写操作,如修改 data 变量  data = newData;  System.out.println("Write: " + data);  } finally {  rwLock.writeLock().unlock(); // 释放写锁  }  }  
}

在这个示例中,SharedResource 类包含一个整数 data 和一个 ReentrantReadWriteLock。read() 方法使用读锁来读取 data,而 write(int newData) 方法使用写锁来修改 data。通过这种方式,多个线程可以同时读取 data,但在写入 data 时,只有一个线程可以执行此操作。

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

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

相关文章

mysql笔记:12. 数据备份与还原

文章目录 一、数据备份1. 备份单个数据库2. 备份多个数据库3. 备份所有数据库 二、数据还原1. mysql命令2. source命令 在操作数据库时&#xff0c;难免会发生一些意外情况造成数据丢失。为了确保数据的安全&#xff0c;需要定期对数据库中的数据进行备份&#xff0c;这样当遇到…

两会声音|中国石化人大代表:要突出战略性新兴产业、未来产业的位置

十四届全国人大二次会议即将闭幕&#xff0c;“新质生产力”首次写入政府工作报告&#xff0c;并出现在了重要位置。政府工作报告主要从推动产业链供应链优化升级、积极培育新兴产业和未来产业、深入推进数字经济创新发展等三个方面进行了阐述和规划。 全国两会期间&#xff0c…

2024 年系统架构设计师(全套资料)

2024年5月系统架构设计师最新第2版教材对应的全套视频教程、历年真题及解析、章节分类真题及解析、论文写作及范文、教材、讲义、模拟题、答题卡等资料 1、2023年11月最新第2版本教材对应全套教程视频&#xff0c;2022年、2021年、2020年、2018年、2016年五套基础知识精讲视频、…

搭建nacos集群,并通过nginx实现负载均衡

nacos、eureka、consul、zookeeper等都是常用的微服务注册中心&#xff0c;这篇文章详细介绍一下在Ubuntu操作系统上搭建一个nacos的集群&#xff0c;以及通过nginx的反向代理功能实现nacos的负载均衡。 目录 一、安装nacos 1、安装nacos 2、修改nacos配置文件 3、创建naco…

MIT 6.858 计算机系统安全讲义 2014 秋季(二)

译者&#xff1a;飞龙 协议&#xff1a;CC BY-NC-SA 4.0 本地客户端 注意&#xff1a; 这些讲座笔记略有修改&#xff0c;来自 2014 年 6.858 课程网站。 本文的目标是什么&#xff1f; 当时&#xff0c;浏览器只允许任何网页运行 JS&#xff08;Flash&#xff09;代码。 希…

【C++】6-13 学生成绩的快速录入(构造函数)分数 10

6-13 学生成绩的快速录入&#xff08;构造函数&#xff09; 分数 10 全屏浏览 切换布局 作者 何振峰 单位 福州大学 现在需要录入一批学生的成绩&#xff08;学号&#xff0c;成绩&#xff09;。其中学号是正整数&#xff0c;并且录入时&#xff0c;后录入学生的学号会比前…

学习JAVA的第十九天(基础)

目录 File 成员方法&#xff08;判断和获取&#xff09; 成员方法&#xff08;创建和删除&#xff09; 成员方法&#xff08;获取并遍历&#xff09; IO流 FileOutputStream FileInputStream 文件拷贝 前言&#xff1a;学习JAVA的第十八天&#xff08;基础&#xff09;…

如果实现了BeanFactoryPostProcessor接口,则@PostConstruct和@PreDestroy和@Value将不起作用

如果实现了BeanFactoryPostProcessor接口,则PostConstruct和PreDestroy和Value将不起作用 如果实现了BeanFactoryPostProcessor接口,则PostConstruct和PreDestroy和Value将不起作用 BeanFactoryPostProcessor BeanFactoryPostProcessor是Spring框架中的一个接口&#xff0c;用…

【C语言】Linux内核pci_read_config_和pci_write_config_

一、pci_read_config_讲解 这些函数是Linux内核中用于从PCI设备的配置空间读取信息的函数。配置空间是PCI设备的一小块内存&#xff0c;它存储了关于该设备的重要信息&#xff0c;例如设备ID、供应商ID、中断设置等。 pci_read_config_byte、pci_read_config_word、pci_read_c…

算法刷题day27:日期问题

目录 引言概念一、日期差值二、日期问题三、回文日期 I四、回文日期 II五、日期计算 引言 日期问题在蓝桥杯中只要把常见的题型掌握明白了&#xff0c;把逻辑给写清楚明白&#xff0c;基本上是很简单的&#xff0c;再就是多做题&#xff0c;题型多见&#xff0c;做熟练&#x…

章六、集合(1)—— 概念、API、List 接口及实现类、集合迭代

零、 关闭IDEA调试时自动隐藏空元素 一、 集合的概念 存储一个班学员信息&#xff0c;假定一个班容纳20名学员 当我们需要保存一组一样&#xff08;类型相同&#xff09;的元素的时候&#xff0c;我们应该使用一个容器来存储&#xff0c;数组就是这样一个容器。 数组有什么缺…

记录一下el-table的tooltip换行

一些需求场景下&#xff0c;需要保持el-table中tooltip出现的时机&#xff0c;并且当前代码编写时完全不能通过js控制tooltip禁用属性时&#xff0c;可以通过以下方法实现tooltip换行。 1、对应单元格的 showOverflowTooltip 属性设置为true&#xff0c;tooltip出现时机依然使…

一文读懂:公网IP地址证书

公网IP证书是一种SSL证书&#xff0c;用于验证和确认特定的公网IP地址是否实际属于申请者。如果验证通过&#xff0c;证书颁发机构将向该IP地址持有人颁发一个以IP地址为主题的SSL证书。使用公网IP证书可以有效提升IP身份的辨识度&#xff0c;减少网站链接被假冒的风险&#xf…

学会这7种SQL进阶用法,让你少走99%的弯路!

引言 在日常业务开发中&#xff0c;熟练掌握SQL语言是至关重要的。除了基础的增删改查操作外&#xff0c;了解和掌握一些进阶的SQL用法能够让你更高效地处理各种复杂的数据操作。本文将介绍几种SQL进阶用法&#xff0c;让你少走99%的弯路&#xff0c;提高数据处理效率。 自定…

【Idea】八种Debug模式介绍

1.行断点 在对应的代码行左侧边栏点击鼠标左键&#xff0c;会出现一个红色圆圈&#xff0c;以debug模式执行时当代码运行到此处则会停止&#xff0c;并可以查询相关上下文参数 2.方法断点 在方法左侧点击创建断点,在方法进入时会停止&#xff0c;同时可以右键断点&#xff0c;…

vite vue3 路由配置@找不到文件问题描述

问题描述 在vite.config.js文件中配置路由的时候&#xff0c;添加路由界面&#xff0c;找不到指定的文件&#xff0c;提示错误&#xff0c;如图所示&#xff1a; 但是换成 ./ 或者 ../ 就正常了&#xff0c;也没有报错问题 解决办法 1.安装一个path的插件 npm install --sav…

风车IM即时通讯系统APP源码DJ2403版完整苹果安卓教程

关于风车IM&#xff0c;你在互联网上能随便下载到了基本都是残缺品&#xff0c; 经过我们不懈努力最终提供性价比最高&#xff0c;最完美的版本&#xff0c; 懂货的朋友可以直接下载该版本使用&#xff0c;经过严格测试&#xff0c;该版本基本完美无缺。 1.宝塔环境如下: Ngin…

第二十四章 跨域

一、跨域 1. 什么是跨域 跨域&#xff0c;是指当前浏览器不能执行其他网站的脚本。它是由浏览器的同源策略造成的&#xff0c;是浏览器对JavaScript实施的安全限制。跨域问题也可以具体描述为&#xff1a;请求方使用XMLHttpRequest请求没有遵守同源策略且没有设置CORS规则的被…

什么是Git引用和分支?

一. 引言 什么是Git引用和分支&#xff1f;比如我在 Github 上一个项目的 .git/refs目录下&#xff1a; ├─heads │ dev │ master │ ├─remotes │ └─origin │ master │ └─tags refs 目录下包含了 heads、remote、tags 三个子目录&#xff0…

openCV制作九宫格图片

我想将任意九张图片按照九宫格排列方式合并成一张大图&#xff0c;使用openCV实现。 如果用画图工具来实现的话&#xff0c;需要事先准备一个600 X 600像素的画布。用openCV实现也是同理&#xff0c;准备一张600 X 600的图片。然后将图片划分成9份&#xff0c;每一份替换成小图…