Java多线程篇(9)——AQS之读写锁(ReentrantReadWriteLock)

news2024/10/1 1:31:54

文章目录

  • 1、读写锁的实现
    • 1.1、state的分割与HoldCounter
    • 1.2、写锁的获取/释放
    • 1.3、读锁的获取/释放
  • 2、写锁降级成读锁的使用场景

在这里插入图片描述

1、读写锁的实现

1.1、state的分割与HoldCounter

ReentrantReadWriteLock 内部维护了读锁和写锁两个锁,这两个锁内部都依赖于同一个sync变量,也即使用了同一个aqs队列。
在这里插入图片描述
写锁:其实就是独占锁的获取释放
在这里插入图片描述
读锁:其实就是共享锁的获取释放
在这里插入图片描述
所以一个sync是如何既实现共享锁又实现独占锁的?换句话说就是一个 state 变量是如何既表示共享锁又表示独占锁的?
答:采用“按位切割使用”的方式来维护这个变量,将其切分为两部分:高16用于共享锁,低16用于独占锁。
写状态,等于 state & 0x0000FFFF(将高 16 位全部抹去)。 当写状态加1,等于 state + 1。
读状态,等于 state >>> 16 (无符号补 0 右移 16 位)。当读状态加1,等于 state +(1<<16),也就是 state + 0x00010000。
获取读锁之前先判断是否有写锁(低16位是否为0),如果没有写锁才可以获取读锁。同理,获取写锁之前先判断是否有读锁或写锁(state是否为0),如果没有任何锁才可以获取写锁。

读写锁:读写,写写互斥,读读不互斥

在这里插入图片描述
exclusiveCount:低16位,写锁(重入)次数。
sharedCount:高16位,读锁的线程数。

在看实际的读写逻辑之前,先思考这么一个问题。
因为读锁是多个线程持有的,所以不能像写锁一样重入次数直接记录在state,那么读锁的重入次数记录在哪?
答:读锁的重入次数记录在 ThreadLocal,每个线程都维护了一个 HoldCounter 计数器。除第一个读锁的线程是直接记录在 firstReaderHoldCount 变量外,其他线程的重入次数都记录在 ThreadLocal。
在这里插入图片描述

1.2、写锁的获取/释放

Sync.tryAcquire

		protected final boolean tryAcquire(int acquires) {
            Thread current = Thread.currentThread();
            
            //得到 state 以及位运算得到 state 的低16位
            int c = getState();
            int w = exclusiveCount(c);

			//如果state不为0则只需判断是否写锁重入的情况
            if (c != 0) {
            	//如果低16位为0,或者当前线程不是独占线程都不是写锁重入的情况
                if (w == 0 || current != getExclusiveOwnerThread())
                    return false;
                if (w + exclusiveCount(acquires) > MAX_COUNT)
                    throw new Error("Maximum lock count exceeded");
                //是写锁重入的情况,state+acquires
                setState(c + acquires);
                return true;
            }
            
            //如果state为0,说明是无锁状态,直接cas(state+acquires)。
            //其中writerShouldBlock()在公平/非公平锁有不同的实现
            //  非公平锁:直接返回false
            //	公平锁:如果队列有节点返回true,反之false
            if (writerShouldBlock() ||
                !compareAndSetState(c, c + acquires))
                return false;
                
            //设置当前独占锁的线程
            setExclusiveOwnerThread(current);
            return true;
        }

Sync.tryRelease

        protected final boolean tryRelease(int releases) {
            if (!isHeldExclusively())
                throw new IllegalMonitorStateException();
            //就是直接的state-releases,然后设值
            int nextc = getState() - releases;
            boolean free = exclusiveCount(nextc) == 0;
            if (free)
            	//如果重入释放完了,就清空独占锁线程
                setExclusiveOwnerThread(null);
            setState(nextc);
            return free;
        }

1.3、读锁的获取/释放

Sync.tryAcquireShared

		protected final int tryAcquireShared(int unused) {
            Thread current = Thread.currentThread();
            int c = getState();
            
            //用于写锁降级的情况
            if (exclusiveCount(c) != 0 &&
                getExclusiveOwnerThread() != current)
                return -1;
                
            //获取state的高16位
            int r = sharedCount(c);
            
            //cas(state+0x00010000)
            //其中readerShouldBlock() 在公平/非公平锁有不同的实现
            // 非公平锁:理论上也是直接返回false,但是为了避免大量的读线程导致写线程的饥饿。
            //			所以这里做了简单的优化:如果队列中第一个等待的线程是写线程,就返回true
            // 公平锁:如果队列有节点返回true,反之false
            if (!readerShouldBlock() &&
                r < MAX_COUNT &&
                compareAndSetState(c, c + SHARED_UNIT)) {
                //如果是第一个线程获取的读锁,就将重入次数记入firstReaderHoldCount 
                if (r == 0) {
                    firstReader = current;
                    firstReaderHoldCount = 1;
                } else if (firstReader == current) {
                    firstReaderHoldCount++;
                }
                //反之记录在 HoldCounter 
                else {
                    HoldCounter rh = cachedHoldCounter;
                    if (rh == null ||
                        rh.tid != LockSupport.getThreadId(current))
                        //readHolds是ThreadLocal的子类
                        cachedHoldCounter = rh = readHolds.get();
                    else if (rh.count == 0)
                        readHolds.set(rh);
                    rh.count++;
                }
                return 1;
            }
            
            // 上面获锁失败,这里通过自旋的方式再次尝试获取读锁,逻辑跟上面的类似。
            return fullTryAcquireShared(current);
        }

Sync.tryReleaseShared

        protected final boolean tryReleaseShared(int unused) {
            Thread current = Thread.currentThread();
            
            //如果是第一个获取读锁的线程 firstReaderHoldCount--
            if (firstReader == current) {
                if (firstReaderHoldCount == 1)
                    firstReader = null;
                else
                    firstReaderHoldCount--;
            } 
            //反之 HoldCounter--
            else {
                HoldCounter rh = cachedHoldCounter;
                if (rh == null ||
                    rh.tid != LockSupport.getThreadId(current))
                    //readHolds是ThreadLocal的子类
                    rh = readHolds.get();
                int count = rh.count;
                if (count <= 1) {
                    readHolds.remove();
                    if (count <= 0)
                        throw unmatchedUnlockException();
                }
                --rh.count;
            }
            
            //cas(state- 0x00010000)
            for (;;) {
                int c = getState();
                int nextc = c - SHARED_UNIT;
                if (compareAndSetState(c, nextc))
                    return nextc == 0;
            }
        }

综上所述,内部设计如下
在这里插入图片描述

2、写锁降级成读锁的使用场景

在上面读锁获取的代码中有这么一段

		protected final int tryAcquireShared(int unused) {
            Thread current = Thread.currentThread();
            int c = getState();
            
            //用于写锁降级的情况
            if (exclusiveCount(c) != 0 &&
                getExclusiveOwnerThread() != current)
                return -1;

			//...
        }

什么场景下写需要锁的降级?
在这里插入图片描述
比如这种先写后读的场景,即写完之后,后续都是读操作。后续读操作的独占锁是没必要的,为提高吞吐量可以把独占锁换成共享锁。
那为什么不是:写锁->写操作->释放写锁->读锁->读操作->释放读锁?即为什么获取读锁要在释放写锁之前?
因为如果先释放写锁再获取读锁,从释放写锁到获取读锁中间可能有其他线程先获取到了写锁并对修改了值,此时对于原来的线程来说就产生了数据前后不一致的问题。(前后两行代码,释放写锁时明明还是好好的,获取读锁的时候就变了)

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

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

相关文章

全网最新最全的软件测试面试题

一、前言 与开发工程师相比&#xff0c;软件测试工程师前期可能不会太深&#xff0c;但涉及面还是很广的。 在一年左右的实习生或岗位的早期面试中&#xff0c;主要是问一些基本的问题。 涉及到的知识主要包括MySQL数据库的使用、Linux操作系统的使用、软件测试框架问题、测试…

线性代数小例子

这样做有什么问题呢&#xff1a; A 2 A > A ( A − E ) 0 > A E A 0 A^2 A > A(A - E) 0> A E \quad A 0 A2A>A(A−E)0>AEA0 上述做法是错误的&#xff0c;这是因为两个矩阵的乘积结果为0&#xff0c;并不能说明这两个矩阵就是0&#xff0c;即上述…

chromadb 0.4.0 后的改动

本文基于一篇上次写的博客&#xff1a;[开源项目推荐]privateGPT使用体验和修改 文章目录 一.上次改好的ingest.py用不了了&#xff0c;折腾了一会儿二.发现privateGPT官方更新了总结下变化效果 三.others 一.上次改好的ingest.py用不了了&#xff0c;折腾了一会儿 pydantic和c…

Web自动化测试的详细流程和步骤

一、什么是web自动化测试 自动化&#xff08;Automation&#xff09;是指机器设备、系统或过程&#xff08;生产、管理过程&#xff09;在没有人或较少人的直接参与下&#xff0c;按照人的要求&#xff0c;经过自动检测、信息处理、分析判断、操纵控制&#xff0c;实现预期的目…

国内机械臂产业的现状

机械臂作为一种重要的工业自动化设备&#xff0c;具有高效、精准、灵活等特点&#xff0c;被广泛应用于制造业、物流、医疗、农业等领域。随着中国制造业的快速发展和自动化水平的提高&#xff0c;国内机械臂产业也迎来了快速发展的机遇。本文将对国内机械臂产业的现状进行综述…

voc数据集格式与yolo数据集格式的区别及相互转化

Pascal VOC数据集是目标检测领域最常用的标准数据集之一&#xff0c;几乎所有检测方向的论文都会给出其在VOC数据集上训练并评测的效果。VOC数据集包含的信息非常全&#xff0c;它不仅被拿来做目标检测&#xff0c;也可以拿来做分割等任务&#xff0c;因此除了目标检测所需的文…

kafka与zookeeper的集群

基础配置 systemctl stop firewalld && systemctl disable firewalld setenforce 0 sed -i s/SELINUXenforcing/SELINUXdisabled/ /etc/selinux/configvi /etc/hosts ip1 node1 ip2 node2 ip3 node3zookeeper介绍 zookeeper是一个分布式的协调服务&#xff0c;主要用…

【广州华锐互动】车辆零部件检修AR远程指导系统有效提高维修效率和准确性

在快速发展的科技时代&#xff0c;我们的生活和工作方式正在被重新定义。这种变化在许多领域都有所体现&#xff0c;尤其是在汽车维修行业。近年来&#xff0c;AR&#xff08;增强现实&#xff09;技术的进步为这个行业带来了前所未有的可能性。通过将AR技术与远程协助系统相结…

无为WiFi的一批服务器

我们在多个地区拥有高速服务器&#xff0c;保证网速给力&#xff0c;刷片无压力 嘿嘿 <?phpinclude("./includes/common.php"); $actisset($_GET[act])?daddslashes($_GET[act]):null; $urldaddslashes($_GET[url]); $authcodedaddslashes($_GET[authcode]);he…

NPDP和PMP,产品经理应该考哪个?

PMP教的是如何做一个项目&#xff0c;NPDP教的是如何做一个产品。 而在一个产品开发过程中&#xff0c;PMP知识体系讲述的是如何给出一个“产品”&#xff0c;NPDP知识体系讲述的是产品开始到结束的过程。虽然产品的生命周期比项目的生命周期长&#xff0c;但从知识体系看&…

考研是为了逃避找工作的压力吗?

如果逃避眼前的现实&#xff0c; 越是逃就越是会陷入痛苦的境地&#xff0c;要有面对问题的勇气&#xff0c;渡过这个困境的话&#xff0c;应该就能一点点地解决问题。 众所周知&#xff0c;考研初试在大四上学期的十二月份&#xff0c;通常最晚的开始准备时间是大三暑假&…

深入了解 GPU 互联技术——NVLINK

随着人工智能和图形处理需求的不断增长&#xff0c;多 GPU 并行计算已成为一种趋势。对于多 GPU 系统而言&#xff0c;一个关键的挑战是如何实现 GPU 之间的高速数据传输和协同工作。然而&#xff0c;传统的 PCIe 总线由于带宽限制和延迟问题&#xff0c;已无法满足 GPU 之间通…

大数据软件开发的数据队列框架

在软件开发中&#xff0c;数据队列框架用于实现消息传递、异步通信和事件驱动的系统。以下是一些常见的数据队列框架和消息中间件&#xff0c;希望对大家有所帮助。北京木奇移动技术有限公司&#xff0c;专业的软件外包开发公司&#xff0c;欢迎交流合作。 1.Apache Kafka&…

小文的美丽转型:从美业学徒到皇嘉集团销售冠军

"" 小文来自山东菏泽的一个农村家庭&#xff0c;她为了供弟弟考上大学&#xff0c;家庭的负担很重&#xff0c;父母收入也不高。因此&#xff0c;小文只读到高一就来到省城的一家综合美容院&#xff0c;学习美甲和美容技术。每天早上起床后&#xff0c;她要打扫卫生&…

桌面文件不见了怎么恢复?4招轻松找回!

桌面文件不见了怎么恢复&#xff1f;4招轻松找回&#xff01; 不知道桌面文件不见了怎么恢复&#xff1f;阅读本文找到答案&#xff0c;这里向你介绍了桌面文件恢复的4种方法&#xff0c;并说明了详细的操作步骤&#xff0c;赶紧来试试&#xff01; 桌面文件突然不见了&…

黑五网一,亚马逊测评系统如何搭建,才能成为重要的运营手段

随着黑五网一购物狂欢节的临近&#xff0c;许多卖家都在积极备货、补单、优化产品&#xff0c;以争取在活动期间获得更好的销售业绩。然而&#xff0c;许多卖家在测评时遭遇了各种问题&#xff0c;例如砍单、掉评以及买卖家账号被封等。 据了解&#xff0c;很多卖家和测评服务…

第八章:关系数据库设计

一、范式 一、BC范式&#xff08;BCNF&#xff09; &#xff08;1&#xff09;定义 BC范式可以消除所有利用函数依赖发现的冗余 范式是对于关系模式而言的&#xff0c;即可以说关系模式R是一个范式。 因此对于BC范式&#xff0c;它需要满足的条件是&#xff0c;对于它的所有函…

网络安全工程师自主学习计划表(具体到阶段目标,保姆级安排,就怕你学不会!)

前言 接下来我将给大家分享一份网络安全工程师自学计划指南&#xff0c;全文将从学习路线、学习规划、学习方法三个方向来讲述零基础小白如何通过自学进阶网络安全工程师&#xff0c;全文篇幅有点长&#xff0c;同学们可以先点个收藏&#xff0c;以免日后错过了。 目录 前言…

Android修改默认system/bin/下可执行程序拥有者和权限,使用实例,只有root和系统app权限才能执行某个命令。

一、执行ls -l /system/bin/ 查看一下用户和权限。 二、这些权限在哪里修改呢? 默认编译system/bin/可执行程序赋予权限的地方system\core\libcutils\fs_config.cpp 文件里面的android_files 三、使用实例,只有root和系统app权限才能执行某个命令,如上面的sn_writer命令,只…

【算法刷题】【链表】链表内指定区间反转:将一个节点数为 size 链表 m 位置到 n 位置之间的区间反转,要求时间复杂度 O(n),空间复杂度)O(1)。

题目 解题 import java.util.*;/** public class ListNode {* int val;* ListNode next null;* public ListNode(int val) {* this.val val;* }* }*/public class Solution {/*** 代码中的类名、方法名、参数名已经指定&#xff0c;请勿修改&#xff0c;直接返回…