初探 JUC 并发编程:读写锁 ReentrantReadWriteLock 原理(8000 字源码详解)

本文中会涉及到一些前面 ReentrantLock 中学到的内容,先去阅读一下我关于独占锁 ReentrantLock 的源码解析阅读起来会更加清晰。
初探 JUC 并发编程:独占锁 ReentrantLock 底层源码解析

6.4)读写锁 ReentrantReadWriteLock 原理

前面提到的 ReentrantLock 是独占锁,某个时间只有一个线程可以获取这个锁,而实际情况中会出现读多写少的情况,ReentrantLock 无法满足这个需求,所以就有了读写锁 ReentrantReadWriteLock。这个锁采用了读写分离的策略,允许多个线程同时获取读锁。

6.4.1)类图结构

在这里插入图片描述

ReentrantReadWriteLock 的类图结构如图所示,类中维护了一个 ReadLock 和 WriteLock,它们依赖 Sync 实现功能,而 Sync 继承自 AQS,也提供了公平和非公平的实现。

下面来看一下 Sync 中的属性和常用方法:

因为读写锁中维护了读锁和写锁两个状态,但是 AQS 只提供了一个 state;读写锁中巧妙的使用了 state 的高 16 位表示读状态,也就是获取到读锁的次数,使用低十六位表示写的次数。

        static final int SHARED_SHIFT   = 16; 
        
        // 读锁状态单位值 65536
        static final int SHARED_UNIT    = (1 << SHARED_SHIFT);
        
        
        // 读锁的状态单位值 65536
        static final int MAX_COUNT      = (1 << SHARED_SHIFT) - 1;
        
        // 写锁掩码,15 个 1
        static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;
        
        /** 返回读锁线程数  */
        static int sharedCount(int c) { 
        // c 一般为 state 的值,将值右移 16 为
	        return c >>> SHARED_SHIFT; 
        }
        
        /** 返回写锁的重入次数  */
        static int exclusiveCount(int c) { 
        // 将值与写锁掩码做与操作
	        return c & EXCLUSIVE_MASK; 
        }
        // 第一个获取到读锁的线程
        private transient Thread firstReader = null;
        // 第一个获取到读锁的线程的可重入次数
        private transient int firstReaderHoldCount;
        // 记录最后一个获取到读锁的可重入次数
        private transient HoldCounter cachedHoldCounter;
        
        static final class HoldCounter {
            int count = 0;
            // 使用 id 而不是引用来避免垃圾保留
            final long tid = getThreadId(Thread.currentThread());
        }

其中 readHolds 是一个 ThreadLocal 变量,存放第一个获取到读线程之外的其他线程读锁的可重入次数,ThreadLocalHoldCounter 继承自 ThreadLocal。
firstReader: 这是一个线程引用,用来记录第一个获得读锁的线程。当锁从无读线程持有(即读锁计数器shareCount为0)变为有读线程持有(即读锁计数器shareCount至少为1)时,这个变量会记录下那个“第一个”读线程。

这样做主要是为了优化后续的读锁获取操作,因为一旦有线程成为了firstReader,它在再次尝试获取读锁时,可以更快地进行,因为它不需要像其他线程那样去更新或检查线程局部的HoldCounter对象。如果这个线程释放了它的所有读锁,导致读锁计数器回到0,那么firstReader会被设置为null

而 cachedHoldCounter 是存储最后一个获取到锁的线程的 id 和 count,是为了减少在常见情况下(即最近释放锁的线程通常是最近获取锁的线程)的线程本地存储(ThreadLocal)查找开销。

        private transient ThreadLocalHoldCounter readHolds;
        
        static final class ThreadLocalHoldCounter
            extends ThreadLocal<HoldCounter> {
            public HoldCounter initialValue() {
                return new HoldCounter();
            }
        }

6.4.2)写锁的获取与释放

        ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
        ReentrantReadWriteLock.WriteLock writeLock = readWriteLock.writeLock();

通过上面的代码获取写锁,写锁和上面的 ReentrantLock 锁都是独占可重入锁,所以方法都差不多,调用 lock 方法,可以获取锁:

        public void lock() {
            sync.acquire(1);
        }
        
		    public final void acquire(int arg) {
		    // 调用 sync 中重写的 tryAcquire 方法
		        if (!tryAcquire(arg) &&
		            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
		            selfInterrupt();
		    }

其中调用了 WriteLock 中重写的 tryAcquire 方法:

        protected final boolean tryAcquire(int acquires) {
            Thread current = Thread.currentThread();
            int c = getState();
            int w = exclusiveCount(c); // 获取写锁的重入次数
            // 1)读锁或者写锁被占有
            if (c != 0) {
            // 1)写锁的重入次数为 0,也就是被读锁占有的情况,如果读锁占有,则 w 不为 0
            // 2)当前线程不是持有写锁的线程
                if (w == 0 || current != getExclusiveOwnerThread())
                    return false;
                // 1)越界的情况
                // 2)如果能走到这里,说明锁被当前线程持有
                if (w + exclusiveCount(acquires) > MAX_COUNT)
                    throw new Error("Maximum lock count exceeded");
                // 重入次数加一
                setState(c + acquires);
                return true;
            }
            // 这个 writerShouldBlock() 方法是 ReentrantReadWriteLock 中的一个抽象方法,
            // 用于确定当前线程在尝试获取写锁时是否应该被阻塞,
            // 具体是因为什么原因阻塞取决于锁的实现和其策略,如果是非公平锁不需要阻塞
            // 非公平锁有阻塞相关的逻辑
            if (writerShouldBlock() ||
                !compareAndSetState(c, c + acquires))
                return false;
            setExclusiveOwnerThread(current);
            return true;
        

上面的方法是写锁中实现的 tryAcquire 方法,方法的执行流程是这样的:首先回去判断状态值是否不等于 0,如果不等于零则说明读锁或者写锁被占有(读锁和写锁不能同时起作用),然后去判断写锁的重入次数是否为 0,如果为 0 则说明当前锁是读锁,无法获取写锁;如果当前锁是写锁的话,去判断锁是否被线程持有,如果被持有,对重入次数做一个自增;如果当前锁没有被占有,则将修改低 16 位的 state 来表明当前锁是写锁状态,且写锁被占有。

和 ReentrantLock 相同,方法中也提供了 lockInterruptibly() 方法、 tryLock() 方法、 tryLock(long timeout, TimeUnit unit) 作用和 ReentarntLock 完全相同,这里不赘述了。

写锁的释放方法是委托给 Sync 类来做的:

    public void unlock() {
        sync.release(1);
    }
        
    public final boolean release(int arg) {
        if (tryRelease(arg)) {
            Node h = head;
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
    }

下面来看核心代码 tryRelease 的实现:

        protected final boolean tryRelease(int releases) {
        // 1)锁未被当前线程持有
            if (!isHeldExclusively())
                throw new IllegalMonitorStateException();
            int nextc = getState() - releases; // 下次修改的值
            boolean free = exclusiveCount(nextc) == 0; // 如果为 0 则完全释放锁
            if (free)
                setExclusiveOwnerThread(null); // 清除持有锁的线程
            setState(nextc);
            return free; // 锁是否被线程持有
        }

6.4.3)读锁的获取与释放

如果当前没有其他线程持有写锁,则当前线程可以获取读锁,AQS 的状态值 state 的高 16 位会增加 1,如果有线程持有写锁的话,获取读锁的线程会被阻塞。

先来看读锁的 lock 方法,同样是委托给 sync 进行的:

    public void lock() {
        sync.acquireShared(1);
    }
        
    public final void acquireShared(int arg) {
        if (tryAcquireShared(arg) < 0)
            doAcquireShared(arg);
    }

接下来看一下在读锁中实现的核心代码, tryAcquireShared()

        protected final int tryAcquireShared(int unused) {
            Thread current = Thread.currentThread();
            int c = getState();
            // 1)写锁被占有
            // 2)写锁不被当前线程持有
            if (exclusiveCount(c) != 0 &&
                getExclusiveOwnerThread() != current)
                return -1;
            // 获取高 16 位的内容
            int r = sharedCount(c);
            // 判断获取读锁的时候是否需要被阻塞
            // 1)本类中的逻辑为判断 AQS 队列中的第一个元素是否在获取写锁
            // 2)共享锁的获取次数没有达到上限
            // 3)当前线程修改 sharedCount 成功
            // 多个线程调用该方法的时候只要一个线程会成功(因为进行 CAS 操作),
            // 未成功的线程会进入 fullTryAcquireShared 方法
            if (!readerShouldBlock() &&
                r < MAX_COUNT &&
                compareAndSetState(c, c + SHARED_UNIT)) {
                // 1)没有线程获取到读锁
                if (r == 0) {
                    firstReader = current;
                    firstReaderHoldCount = 1;
                // 1)当前线程是第一个获取到读锁的线程
                } else if (firstReader == current) {
                    firstReaderHoldCount++;
                } else {
                    HoldCounter rh = cachedHoldCounter; // 最后一个获取到读锁的线程
                    // 1)最后一个获取到读锁的线程为 null
                    // 2)最后一个获取到锁的线程不是当前线程
                    if (rh == null || rh.tid != getThreadId(current))
                    // 将 cachedHoldCounter 设置为当前线程
                        cachedHoldCounter = rh = readHolds.get();
                    // 1)最后一个获取到锁的线程为当前线程
                    else if (rh.count == 0)
                    // 确保 readHolds 被初始化
                        readHolds.set(rh);
                    rh.count++;
                }
                return 1;
            }
            return fullTryAcquireShared(current);
        }

上面的代码中首先检查是否有其他线程获取到了写锁,如果有则直接返回 -1,之后会将当前线程放到 AQS 阻塞队列。如果当前获取读锁的线程持有写锁,则可以直接获取读锁,但注意释放锁的时候将两个锁都释放掉。

本类中的 readerShouldBlock() 方法是这样的:

	  // 避免重复获取读锁导致写锁无法被获取的情况
	  final boolean readerShouldBlock() {
        return apparentlyFirstQueuedIsExclusive();
    }
    // 当前 AQS 队列中收个节点请求的是写锁
    final boolean apparentlyFirstQueuedIsExclusive() {
        Node h, s;
        return (h = head) != null &&
            (s = h.next)  != null &&
            !s.isShared()         &&
            s.thread != null;
    }

当多次获取读锁可能会导致写锁持续被阻塞,所以当发现 AQS 队列中首个节点请求的是写锁的时候,获取读锁的线程暂时阻塞给写锁让步。

因为多个线程只有一个会获取写锁,剩余的情况在 tryAcquireShared() 中并没有被处理

  1. 有线程获取写锁的时候,被阻塞
  2. CAS 操作失败

这时候就调用 fullTryAcquireShared,这个方法会循环自旋的获取读锁:

        final int fullTryAcquireShared(Thread current) {
            HoldCounter rh = null;
            for (;;) {
                int c = getState();
                // 1)写锁被占有
                if (exclusiveCount(c) != 0) {
		                // 1)写锁不被当前线程持有
                    if (getExclusiveOwnerThread() != current)
                        return -1;
                // 1)当前线程应该被阻塞
                } else if (readerShouldBlock()) {
                    if (firstReader == current) {
                        // assert firstReaderHoldCount > 0;
                    } else {
                        if (rh == null) {
                            rh = cachedHoldCounter;
                            // 1)最后一个获取读锁的线程为空
                            // 或者
                            // 2)最后一个获取到锁的线程未被设置为本线程
                            if (rh == null || rh.tid != getThreadId(current)) {
                            // 判断当前线程是否获取过锁
                                rh = readHolds.get();
                                if (rh.count == 0)
                                // 未获取过的话,清除 readHolds
                                    readHolds.remove();
                            }
                        }
                        // 当前线程被阻塞了
                        if (rh.count == 0)
                            return -1;
                    }
                }
                
	              // 执行到这里说明写锁没有被占有,且当前线程没有被阻塞,可以尝试获取锁
                if (sharedCount(c) == MAX_COUNT)
                // 越界的情况
                    throw new Error("Maximum lock count exceeded");
                    
                // 使用 CAS 操作修改 state,给获取读锁的线程数加一
                if (compareAndSetState(c, c + SHARED_UNIT)) {
                // 如果当前锁没有被线程占用
                    if (sharedCount(c) == 0) {
                        firstReader = current;
                        firstReaderHoldCount = 1;
                     // 第一个持有锁的线程为当前线程
                    } else if (firstReader == current) {
                        firstReaderHoldCount++;
                    } else {
                        if (rh == null)
                            rh = cachedHoldCounter;
                        // cachedHoldCounter 为空,或不为当前线程
                        if (rh == null || rh.tid != getThreadId(current))
                            rh = readHolds.get(); // 设置 rh
                        else if (rh.count == 0)
                            readHolds.set(rh);
                        rh.count++; // 自增
                        cachedHoldCounter = rh; // cache for release
                    }
                    return 1;
                }
            }
        }

上面的方法中,先去判断写锁有没有被占有,如果被占有则直接返回 -1。

然后去判断当前线程是否应该被阻塞,也就是 AQS 队列的队头是不是请求的写锁,然后去判断最后一个获取到锁的线程是不是本线程,如果不是的话,检查线程中的 readHolds 是否为 0(如果为 0 则说明没有获取到锁,如果获取到了锁这里应该置为 1),因为 get 方法会向线程的 ThreadLocal 中添加对象,所以在确定它没有得到锁之后清楚 ThreadLocal 中的内容。

如果上面的代码均通过,说明写锁没有被占有,且当前线程没有被阻塞,可以尝试获取锁,其中获取锁的方法和上面相同。

同样的,读锁中也存在 tryLock 等方法,这里不做过多赘述。

然后来看释放锁的方法,这里的释放锁也是委托给 Sync 类进行的:

    public void unlock() {
        sync.releaseShared(1);
    }
    
    public final boolean releaseShared(int arg) {
        if (tryReleaseShared(arg)) {
            doReleaseShared();
            return true;
        }
        return false;
    }        

其中核心方法是 Sync 的实现类中实现的 tryReleaseShared 方法:

        protected final boolean tryReleaseShared(int unused) {
            Thread current = Thread.currentThread();
            // 1)当前线程是第一个获取到读锁的线程
            if (firstReader == current) {
                if (firstReaderHoldCount == 1)
                    firstReader = null;
                else
                    firstReaderHoldCount--;
            } else {
                HoldCounter rh = cachedHoldCounter;
                // 1)当前线程不是最后一个获取到锁的线程
                if (rh == null || rh.tid != getThreadId(current))
                    rh = readHolds.get();
                // 检查重入次数
                int count = rh.count;
                // 1)锁已经释放完成,可以清除了
                if (count <= 1) {
                    readHolds.remove();
                    // 如果是 0 表示未获取到锁
                    if (count <= 0)
                        throw unmatchedUnlockException();
                }
                --rh.count;
            }
            // 减少一次重入次数
            for (;;) {
                int c = getState();
                int nextc = c - SHARED_UNIT;
                if (compareAndSetState(c, nextc))
                    return nextc == 0;
            }
        }

方法中先对线程是否为 firstReader 或者 cachedHoldCounter 做了判断,对其进行特殊的处理,然后检查重入的次数,如果次数小于等于一,则本次释放就将线程持有的读锁全部释放完成,此时删除线程 ThreadLocal 中的内容;最后循环减少可重入次数。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

谈基于ATTCK框架的攻击链溯源

引言 网络安全在当今数字化时代变得尤为关键&#xff0c;而MITRE公司开发的ATT&CK框架则成为了安全专业人员的重要工具。ATT&CK是一种广泛使用的攻击行为分类和描述框架。其目的在于提供一个共同的语言&#xff0c;使安全专业人员能够更好地理解攻击者的行为和目标&…

整理好了!咸阳市各区县高新技术企业申报奖补标准,高企认定时间流程及申报条件

咸阳市及各区县高企申报奖励 咸阳市&#xff1a;对首次通过认定的高新技术企业给予20万元的奖励&#xff0c;通过复审的企业给予5万元奖励。政策依据&#xff1a;咸阳市人民政府办公室关于印发《咸阳市科技型企业三年倍增计划实施方案&#xff08;2022—2024年&#xff09;》的…

如何在您的WordPress网站上安装和设置W3 Total Cache

本周有一个客户&#xff0c;购买Hostease的虚拟主机&#xff0c;询问我们的在线客服&#xff0c;如何在您的WordPress网站上安装和设置W3 Total Cache&#xff1f;我们为用户提供相关教程&#xff0c;用户很快解决了遇到的问题。在此&#xff0c;我们分享这个操作教程&#xff…

【2022 深圳 ArchSummit 】大数据架构稳定性保障实践

文章目录 一、前言二、现状三、大数据架构的历史变迁&#xff08;一&#xff09;洪荒期&MR&#xff08;二&#xff09;远古期&MPP&#xff08;四&#xff09;近现代&Flink/Spark&#xff08;五&#xff09;现如今&实时数据湖架构 四、架构稳定的关键因素&#…

学习100个Unity Shader (17) --- 深度纹理

文章目录 效果shader部分C# 部分理解参考 效果 shader部分 Shader "Example/DepthTexture" {SubShader{Pass{CGPROGRAM#pragma vertex vert#pragma fragment frag#include "UnityCG.cginc"sampler2D _CameraDepthTexture;struct a2v{float4 pos : POSITIO…

公司活动想找媒体报道宣传怎样联系媒体?

作为公司宣传负责人,我深知媒体报道对于企业活动宣传的重要性。然而,在过去,每当有重要活动需要媒体曝光时,我总会被繁琐的媒体联系工作所困扰。 那时,我需要一家家地查询媒体联系方式,发送邮件、打电话,甚至亲自前往媒体机构进行沟通。然而,这样的过程不仅费时费力,而且效率低…

Linux系统调用过程详解:应用程序调用驱动过程

Linux下应用程序调用驱动程序过程&#xff1a; &#xff08;1&#xff09;加载一个驱动模块(.ko)&#xff0c;产生一个设备文件&#xff0c;有唯一对应的inode结构体 a、每个设备文件都有一个对应的’inode‘结构体&#xff0c;包含了设备的主次设备号&#xff0c;是设备的唯一…

ChatGLM3-6B部署与微调及微调后使用

记录ChatGLM3-6B部署及官方Lora微调示例详细步骤及如何使用微调后的模型进行推理 一、下载代码 使用git clone 命令下载源码 git clone https://github.com/THUDM/ChatGLM3.git 如图所示 二、下载模型 模型权重文件从魔塔进行下载&#xff0c;不需要翻墙。权重文件比较大&…

搭建知识库必备:12个开源 Wiki 软件工具盘点

在任何成功的公司中&#xff0c;部门间的知识共享是至关重要的。如果没有一个简单的信息交流方法&#xff0c;团队怎样才能有效合作呢&#xff1f;Wiki软件提供了一种创建、组织及在全公司范围内分享知识的直接方法。但是&#xff0c;哪一种Wiki软件是最佳的选择呢&#xff1f;…

【计算机毕业设计】springboot工资管理系统

人类现已迈入二十一世纪&#xff0c;科学技术日新月异&#xff0c;经济、资讯等各方面都有了非常大的进步&#xff0c;尤其是资讯与 网络技术的飞速发展&#xff0c;对政治、经济、军事、文化等各方面都有了极大的影响。 利用电脑网络的这些便利&#xff0c;发展一套工资管理系…

Unity 修复Sentinel key not found (h0007)错误

这个问题是第二次遇到了&#xff0c;上次稀里糊涂的解决了&#xff0c;也没当回事&#xff0c;这次又跑出来了&#xff0c;网上找的教程大部分都是出自一个人。 1.删除这个路径下的文件 C:\ProgramData\SafeNet Sentinel&#xff0c;注意ProgramData好像是隐藏文件 2.在Windows…

Mac安装激活--Typora,一个比记事本更加强大的纯文本软件

一、安装 1.首先到官网下载Mac版的Typora,下载地址&#xff1a;https://typoraio.cn/ &#xff08;1&#xff09;打开默认中文站 &#xff08;2&#xff09;往下滑&#xff0c;下载Mac版 2.下载完成后&#xff0c;会看到Typora.dmg文件&#xff0c;点击打开文件 3.打开Typ…

mac苹果电脑卡顿反应慢如何解决?2024最新免费方法教程

苹果电脑以其稳定的性能、出色的设计和高效的操作系统&#xff0c;赢得了广大用户的喜爱。然而&#xff0c;随着时间的推移&#xff0c;一些用户会发现自己的苹果电脑开始出现卡顿、反应慢等问题。这不仅影响使用体验&#xff0c;还会影响工作效率。那么&#xff0c;面对这些问…

luceda ipkiss教程 68:通过代码模板提高线路设计效率

在用ipkiss设计器件或者线路时&#xff0c;经常需要输入: from ipkiss3 import all as i3那么有什么办法可以快速输入这段代码呢&#xff1f;这里就可以利用Pycharm的 live template功能&#xff0c;只需要将文件&#xff1a;ipkiss.xml &#xff08;luceda ipkiss教程 68&…

JetBrains的Java集成开发环境IntelliJ 2024.1版本在Windows/Linux系统的下载与安装配置

目录 前言一、IntelliJ在Windows安装二、IntelliJ在Linux安装三、Windows下使用配置四、Linux下使用配置总结 前言 ​ “ IntelliJ IDEA Ultimate是一款功能强大的Java集成开发环境&#xff08;IDE&#xff09;。它提供了丰富的功能和工具&#xff0c;可以帮助开发人员更高效地…

深入理解Java HashSet类及其实现原理

哈喽&#xff0c;各位小伙伴们&#xff0c;你们好呀&#xff0c;我是喵手。运营社区&#xff1a;C站/掘金/腾讯云&#xff1b;欢迎大家常来逛逛 今天我要给大家分享一些自己日常学习到的一些知识点&#xff0c;并以文字的形式跟大家一起交流&#xff0c;互相学习&#xff0c;一…

LabVIEW MEMS电容式压力传感器测试系统

LabVIEW MEMS电容式压力传感器测试系统 随着微电子技术的发展&#xff0c;MEMS&#xff08;微电机系统&#xff09;技术在各个领域得到了广泛应用。MEMS电容式压力传感器以其高灵敏度、小尺寸、低功耗等优点&#xff0c;在微传感器领域占据了重要的地位。然而&#xff0c;这些…

基于FPGA的音视频监视器,音视频接口采集器的应用

① 支持1路HDMI1路SDI 输入 ② 支持1路HDMI输出 ③ 支持1080P高清屏显示实时画面以 及叠加的分析结果 ④ 支持同时查看波形图&#xff08;亮度/RGB&#xff09;、 直方图、矢量图 ⑤ 支持峰值对焦、斑马纹、伪彩色、 单色、安全框遮幅标记 ⑥ 支持任意缩放画面&#xff0c;支…

TypeScript安装及编译

一、TypeScript是什么 ​ Type script 是微软基于 Javascript 开发的开源编程语言&#xff0c;是拥有类型的 Javascript 的超集&#xff0c;继承了js 所有语法&#xff0c;此外增加了一些自己语法。可以编译成普通、千净、完整的 JavaScript 代码。 目的&#xff1a; 不是创造…

【Linux】从零开始认识动静态库 - 静态库

送给大家一句话: 永不言弃&#xff0c;就是我的魔法&#xff01; ——阿斯塔《黑色四叶草》 ଘ(੭ˊ꒳​ˋ)੭✧ଘ(੭ˊ꒳​ˋ)੭✧ଘ(੭ˊ꒳​ˋ)੭✧ ଘ(੭ˊ꒳​ˋ)੭✧ଘ(੭ˊ꒳​ˋ)੭✧ଘ(੭ˊ꒳​ˋ)੭✧ ଘ(੭ˊ꒳​ˋ)੭✧ଘ(੭ˊ꒳​ˋ)੭✧ଘ(੭ˊ꒳​ˋ)੭✧ 从零…