并发编程之ReentrantReadWriteLock详解

news2024/11/14 20:26:16

目录

ReentrantReadWriteLock介绍

线程进入读锁的前提条件

线程进入写锁的前提条件

ReentrantReadWriteLock三个重要的特性

ReentrantReadWriteLock类

ReentrantReadWriteLock使用读写锁

锁降级

注意事项

ReentrantReadWriteLock结构

 ReentrantReadWriteLock读写状态设计

HoldCounter 计数器

写锁的获取

写锁的释放

读锁的获取

读锁的释放


ReentrantReadWriteLock介绍

        ReentrantReadWriteLock是Java中的一个读写锁实现,它允许多个线程同时读取共享资源(读读可以并发),但在写入时只允许一个线程独占(读写,写读,写写互斥)。这个锁支持重入,即同一个线程可以多次获取相同类型的锁。这种锁可以提高读操作的并发性,同时保证写操作的独占性。在读多于写的情况下,读写锁能够提供比排它锁更好的并发性和吞吐量。

线程进入读锁的前提条件

没有其他线程的写锁

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

线程进入写锁的前提条件

没有其他线程的读锁

没有其他线程的写锁

ReentrantReadWriteLock三个重要的特性

公平选择性:支持非公平(默认)和公平的锁获取方式,吞吐量还是非公平优于公平。

可重入:读锁和写锁都支持线程重入。以读写线程为例:读线程获取读锁后,能够再次获取读锁。写线程在获取写锁之后能够再次获取写锁,同时也可以获取读锁。

锁降级:遵循获取写锁、再获取读锁最后释放写锁的次序,写锁能够降级成为读锁。


ReentrantReadWriteLock类

 ReentrantReadWriteLock实现了ReadWriteLock接口

                  

        ReentrantReadWriteLock是可重入的读写锁实现类。在它内部,维护了一对相关的锁,一个用于只读操作,另一个用于写入操作。只要没有 Writer 线程,读锁可以由多个 Reader 线程同时持有。也就是说,写锁是独占的,读锁是共享的。

ReentrantReadWriteLock使用读写锁
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class SharedData {
    private int data = 0;
    private ReadWriteLock lock = new ReentrantReadWriteLock();

    public int readData() {
        lock.readLock().lock();
        try {
            System.out.println(Thread.currentThread().getName() + " is reading data: " + data);
            return data;
        } finally {
            lock.readLock().unlock();
        }
    }

    public void writeData(int newValue) {
        lock.writeLock().lock();
        try {
            System.out.println(Thread.currentThread().getName() + " is writing data: " + newValue);
            data = newValue;
        } finally {
            lock.writeLock().unlock();
        }
    }

    public static void main(String[] args) {
        SharedData sharedData = new SharedData();

        // 创建多个读线程
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                sharedData.readData();
            }, "ReaderThread-" + i).start();
        }

        // 创建一个写线程
        new Thread(() -> {
            sharedData.writeData(42);
        }, "WriterThread").start();
    }
}

       SharedData 类包含一个整数 data,并使用 ReentrantReadWriteLock 对其进行读写保护。多个读线程(ReaderThread)可以同时访问 readData 方法,而写线程(WriterThread)在调用 writeData 方法时会独占写锁,以确保写入的原子性。多个线程能够同时读取数据,但只有一个线程能够写入新的数据。(ReentrantReadWriteLock适合读多写少的场景)

锁降级

        在互斥锁中,锁是排他的,即一旦线程获取了写锁,其他线程就不能获取读锁或写锁,直到写锁被释放。而ReentrantReadWriteLock允许降级,即允许一个线程在持有写锁的情况下获取读锁,并最终释放写锁,而保持读锁的持有。

import java.util.concurrent.locks.ReentrantReadWriteLock;

public class LockDowngradeExample {
    private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();

    public void performWrite() {
        lock.writeLock().lock(); // 获取写锁
        try {
            // 执行写操作
            System.out.println("Performing write operation...");
            
            // 获取读锁(锁降级)
            lock.readLock().lock();
        } finally {
            lock.writeLock().unlock(); // 释放写锁
        }

        try {
            // 执行读操作
            System.out.println("Performing read operation...");
        } finally {
            lock.readLock().unlock(); // 释放读锁
        }
    }

    public static void main(String[] args) {
        LockDowngradeExample example = new LockDowngradeExample();
        example.performWrite();
    }
}

        在以上代码中,线程首先获取写锁,执行写操作,然后将写锁降级为读锁。在读锁的情况下可以执行读操作,而不会阻塞其他线程获取读锁。这个降级的过程就是锁降级。

注意事项

读锁不支持条件变量。

重入时升级不支持:持有读锁的情况下去获取写锁,会导致获取永久等待。

重入时支持降级: 持有写锁的情况下可以去获取读锁。


ReentrantReadWriteLock结构


 ReentrantReadWriteLock读写状态设计

       在 ReentrantLock 中,使用 Sync ( 实际是 AQS )的 int 类型的 state 来表示同步状态,表示锁被一个线程重复获取的次数。但是,读写锁 ReentrantReadWriteLock 内部维护着一对读写锁,如果要用一个变量维护多种状态,需要采用“按位切割使用”的方式来维护这个变量,将其切分为两部分:高16为表示读,低16为表示写。

通过位运算确定读锁和写锁的状态:

写状态,等于 S & 0x0000FFFF(将高 16 位全部抹去)。 当写状态加1,等于S+1.

读状态,等于 S >>> 16 (无符号补 0 右移 16 位)。当读状态加1,等于S+(1<<16),也就是S+0x00010000

根据状态的划分能得出一个推论:S不等于0时,当写状态(S&0x0000FFFF)等于0时,则读状态(S>>>16)大于0,即读锁已被获取。

 exclusiveCount(int c) 静态方法,获得持有写状态的锁的次数。

sharedCount(int c) 静态方法,获得持有读状态的锁的线程数量。不同于写锁,读锁可以同时被多个线程持有。而每个线程持有的读锁支持重入的特性,所以需要对每个线程持有的读锁的数量单独计数,这就需要用到 HoldCounter 计数器。

HoldCounter 计数器

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

 HoldCounter是用来记录读锁重入数的对象

ThreadLocalHoldCounter是ThreadLocal变量,记录了当前线程在持有读锁的次数。


写锁的获取

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

protected final boolean tryAcquire(int acquires) {
    //当前线程
    Thread current = Thread.currentThread();
    //获取state状态   存在读锁或者写锁,状态就不为0
    int c = getState();
    //获取写锁的重入数
    int w = exclusiveCount(c);
    //当前同步状态state != 0,说明已经有其他线程获取了读锁或写锁
    if (c != 0) {
        // c!=0 && w==0 表示存在读锁
        // 当前存在读锁或者写锁已经被其他写线程获取,则写锁获取失败
        if (w == 0 || current != getExclusiveOwnerThread())
            return false;
        // 超出最大范围  65535
        if (w + exclusiveCount(acquires) > MAX_COUNT)
            throw new Error("Maximum lock count exceeded");
        //同步state状态
        setState(c + acquires);
        return true;
    }
    // writerShouldBlock有公平与非公平的实现, 非公平返回false,会尝试通过cas加锁
    //c==0 写锁未被任何线程获取,当前线程是否阻塞或者cas尝试获取锁 
    if (writerShouldBlock() ||
        !compareAndSetState(c, c + acquires))
        return false;

    //设置写锁为当前线程所有
    setExclusiveOwnerThread(current);
    return true;

writerShouldBlock写锁是否阻塞实现取决公平与非公平的策略(FairSync和NonfairSync)

      

写锁的释放

写锁释放通过重写AQS的tryRelease方法实现

protected final boolean tryRelease(int releases) {
    //若锁的持有者不是当前线程,抛出异常
    if (!isHeldExclusively())
        throw new IllegalMonitorStateException();
    int nextc = getState() - releases;
    //当前写状态是否为0,为0则释放写锁
    boolean free = exclusiveCount(nextc) == 0;
    if (free)
        setExclusiveOwnerThread(null);
    setState(nextc);
    return free;

                                 

读锁的获取

通过重写AQS的tryAcquireShared方法和tryReleaseShared方法。读锁的获取实现方法如下:

protected final int tryAcquireShared(int unused) {
    Thread current = Thread.currentThread();
    int c = getState();
    // 如果写锁已经被获取并且获取写锁的线程不是当前线程,当前线程获取读锁失败返回-1   判断锁降级
    if (exclusiveCount(c) != 0 &&
        getExclusiveOwnerThread() != current)
        return -1;
    //计算出读锁的数量
    int r = sharedCount(c);
    /**
    * 读锁是否阻塞    readerShouldBlock()公平与非公平的实现
    * r < MAX_COUNT: 持有读锁的线程小于最大数(65535)
    *  compareAndSetState(c, c + SHARED_UNIT) cas设置获取读锁线程的数量
    */
    if (!readerShouldBlock() &&
        r < MAX_COUNT &&
        compareAndSetState(c, c + SHARED_UNIT)) {   //当前线程获取读锁
        
        if (r == 0) {  //设置第一个获取读锁的线程
            firstReader = current; 
            firstReaderHoldCount = 1;  //设置第一个获取读锁线程的重入数
        } else if (firstReader == current) { // 表示第一个获取读锁的线程重入
            firstReaderHoldCount++;
        } else { // 非第一个获取读锁的线程
            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);

 readerShouldBlock读锁是否阻塞实现取决公平与非公平的策略(FairSync和NonfairSync)

    

读锁的释放

读锁释放的实现主要通过方法tryReleaseShared:

protected final boolean tryReleaseShared(int unused) {
    Thread current = Thread.currentThread();
    //如果当前线程是第一个获取读锁的线程
    if (firstReader == current) {
        // assert firstReaderHoldCount > 0;
        if (firstReaderHoldCount == 1)
            firstReader = null;
        else
            firstReaderHoldCount--; //重入次数减1
    } 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;  //重入次数减1
    }
    for (;;) {  //cas更新同步状态
        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;
    }

                                       

 本文参考自图灵学院Fox老师笔记!

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

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

相关文章

Github上传代码/删除仓库/新建分支的操作流程记录

首先先安装git&#xff0c;安装完git后&#xff0c;看如下操作指令&#xff1a; 输入自己的用户名和邮箱&#xff08;为注册GITHUB账号时的用户名和邮箱&#xff09;&#xff1a; git config --global user.name "HJX-exoskeleton" git config --global user.email …

Kafka基本介绍

消息队列 产生背景 消息队列&#xff1a;指的数据在一个容器中&#xff0c;从容器中一端传递到另一端的过程 消息(message): 指的是数据&#xff0c;只不过这个数据存在一定流动状态 队列(queue): 指的容器&#xff0c;可以存储数据&#xff0c;只不过这个容器具备FIFO(先进…

Mysql事务的处理

1、事务&#xff0c;就是一组命令的操作。 不过这一组命令&#xff0c;我们有时候需要使用手动提交&#xff1b; 1、使用这组命令可以查询出来现在的提交方式&#xff1a;自动提交&#xff08;就是命令输入&#xff0c;点击enter后&#xff0c;会不会直接对表格产生修改&#x…

x-cmd pkg | csview - 美观且高性能的 csv 数据查看工具

目录 介绍首次用户功能特点类似工具与竞品进一步阅读 介绍 csview 是一个用于在命令行中查看 CSV 文件的工具&#xff0c;采用 Rust 语言编写的&#xff0c;支持中日韩/表情符号。它允许用户在终端中以表格形式查看 CSV 数据&#xff0c;可以对数据进行排序、过滤、搜索等操作…

关于html导出word总结一

总结 测试结果不理想&#xff0c;html-to-docx 和 html-docx-js 最终导出的结果 都 差强人意&#xff0c;效果可以见末尾的附图 环境 "electron": "24.3.0" 依赖库 html-docx-js html-docx-js - npm html-to-docx html-to-docx - npm file-saver…

Linux技术,winSCP连接服务器超时故障解决方案

知识改变命运&#xff0c;技术就是要分享&#xff0c;有问题随时联系&#xff0c;免费答疑&#xff0c;欢迎联系&#xff01; 故障现象 使用 sftp 协议连接主机时, 明显感觉缓慢且卡顿,并且时常出现如下报错: 点击重新连接后,又有概率重新连接上; 总之在"连接上"和&…

前端js调用Lodop实现云打印

一、下载Lodop控件 官网&#xff1a;下载中心 - Lodop和C-Lodop官网主站 二、解压后安装 双击进行安装&#xff0c;里面有些页面文件是一些教程案例 勾选云服务工作模式 安装成功会自动启动 浏览器访问地址&#xff1a;http://localhost:8000/ 首页最下面有个教程案例跳转地址&…

kali_linux换源教程

vim /etc/apt/sources.list #阿里云deb http://mirrors.aliyun.com/kali kali-rolling main non-free contribdeb-src http://mirrors.aliyun.com/kali kali-rolling main non-free contrib#清华大学源deb http://mirrors.tuna.tsinghua.edu.cn/kali kali-rolling main contrib…

navicat for oracle

前言 Oracle中的概念并不是创建数据库&#xff0c;而是创建一个表空间&#xff0c;然后再创建一个用户&#xff0c;设置该用户的默认表空间为我们新创建的表空间&#xff0c;这些操作之后&#xff0c;便和你之前用过的mysql数据库创建完数据库一模一样了。 创建数据库 使用O…

PHP信息分类网源码带手机端和文档

PHP信息分类网源码带手机端和文档 安装简易说明&#xff1a; 上传 → 安装 → 进入后台 → 恢复数据 → 修改cookie记录值&#xff08;第3点有说明&#xff09; 1.上传程序到网站根目录,访问http://域名/install/index.php 进行安装,不要直接打开网址&#xff0c;先直接安装&am…

Java学习手册——第六篇输入输出

几乎所有的开发语言都会有输入输出&#xff0c;程序的定义里面也有输入输出&#xff0c;可以见得输入输出是何等的重要。如果没有输入&#xff0c;人类如何与计算机交流&#xff1f;如果没有输出&#xff0c;一切努力都是白费&#xff0c;因为我们看不到结果。 这里的输入输出你…

常用计算电磁学算法特性与电磁软件分析

常用计算电磁学算法特性与电磁软件分析 参考网站&#xff1a; 计算电磁学三大数值算法FDTD、FEM、MOM ADS、HFSS、CST 优缺点和应用范围详细教程 ## 基于时域有限差分法的FDTD的计算电磁学算法&#xff08;含Matlab代码&#xff09;-框架介绍 参考书籍&#xff1a;The finite…

Multi-Concept Customization of Text-to-Image Diffusion——【代码复现】

本文是发表于CVPR 2023上的一篇论文&#xff1a;[2212.04488] Multi-Concept Customization of Text-to-Image Diffusion (arxiv.org) 一、引言 本文主要做的工作是对stable-diffusion的预训练模型进行微调&#xff0c;需要的显存相对较多&#xff0c;论文中测试时是在两块GP…

原子操作类原理剖析

UC包提供了一系列的原子性操作类&#xff0c;这些类都是使用非阻塞算法CAS实现的&#xff0c;相比使用锁实现原子性操作这在性能上有很大提高。 由于原子性操作类的原理都大致相同&#xff0c;所以只讲解最简单的AtomicLong类的实现原理以及JDK8中新增的LongAdder和LongAccumu…

领导风格测试

领导风格指的是管理者在开展管理工作时的思维和行为模式&#xff0c;通常我们也称之为习惯&#xff0c;或者是人格特征。这种习惯是固化的&#xff0c;是长期的经历所形成的&#xff0c;其中包含了个人的知识&#xff0c;经验&#xff0c;人际关系等。领导风格测试是企业人才选…

必练的100道C语言程序设计练习题(上)

前言: 在计算机编程的世界中&#xff0c;C语言一直是一门备受推崇的语言。它的简洁性、高效性以及广泛应用使得学习C语言成为每一位程序员的必由之路。然而&#xff0c;掌握这门语言并不是一蹴而就的事情&#xff0c;它需要不断的练习和实践。为了帮助各位编程爱好者更好地理解…

进程和线程的比较

目录 一、前言 二、Linux查看进程、线程 2.1 Linux最大进程数 2.2 Linux最大线程数 2.3 Linux下CPU利用率高的排查 三、线程的实现 四、上下文切换 五、总结 一、前言 进程是程序执行相关资源&#xff08;CPU、内存、磁盘等&#xff09;分配的最小单元&#xff0c;是一…

Halcon边缘滤波器edges_image 算子

Halcon边缘滤波器edges_image 算子 基于Sobel滤波器的边缘滤波方法是比较经典的边缘检测方法。除此之外&#xff0c;Halcon也提供了一些新式的边缘滤波器&#xff0c;如edges_image算子。它使用递归实现的滤波器&#xff08;如Deriche、Lanser和Shen&#xff09;检测边缘&…

【无标题】关于异常处理容易犯的错

一般项目是方法打上 try…catch…捕获所有异常记录日志&#xff0c;有些会使用 AOP 来进行类似的“统一异常处理”。 其实&#xff0c;这种处理异常的方式非常不可取。那么今天&#xff0c;我就和你分享下不可取的原因、与异常处理相关的坑和最佳实践。 捕获和处理异常容易犯…

Java研学-分页查询

一 分页概述 1 介绍 将大量数据分段显示&#xff0c;避免一次性加载造成的内存溢出风险 2 真假分页 ① 真分页   一次性查询出所有数据存到内存&#xff0c;翻页从内存中获取数据&#xff0c;性能高但易造成内存溢出 ② 假分页   每次翻页从数据库中查询数据&#xff0c…