源码级丨AQS 读写锁 ReentrantReadWriteLock 的读写状态是如何存储的捏?

源码篇 预防针💉:

源码篇 必然会有很多代码,希望大家不要有畏难情绪,虽然我也是看到一大串代码就头疼。 我贴代码只是为了方便我的文字解答,源码只是辅助我文字讲解,所以大家尽量关注我的文字就好啦。

前言

ReentrantReadWriteLock 也叫 读写锁 ,支持 排他/共享 特性是他的特点。

互斥性 读锁 写锁
读锁 🟩 🟥
写锁 🟥 🟥
  • 读读:共享
  • 读写:互斥
  • 写写:互斥

对于 多读少写 的场景来说,使用 读写锁 可以提高整体 读的吞吐量 。这是一种 锁粒度细化 的体现。

本篇是介绍 ReentrantReadWriteLock 的第一篇,首先我们应该从哪里开始入手呢?

本篇着重从 读写锁的状态存储 进行介绍。中间会介绍在 AQS 中 Sync 到底扮演了一个什么角色?为什么说 AQS 是一个同步框架?

找到线索入口

我们该从源码的哪里入手呢?

具体而言我们应该去看哪个方法捏?

肯定是要看 核心的加锁 逻辑!

先把 读锁 ReadLock 和 写锁 WriteLock 部分核心代码贴一下方便讲解。

后面我就直接用 读锁 和 写锁 简称了

读锁:

java 复制代码
    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));
        }

    }

写锁:

java 复制代码
    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));
        }

        }
    }

我们可以看到在 ReadLock#lock 里面有一个非常现眼包的方法调用:sync.acquireShared,按理来说我们直接去看这个方法去顺藤摸瓜就行了,答案非常明显。

但其实我这里还想让你注意到 WriteLock#lock 里面的 sync.acquire(1),这个是不是很熟悉?

没错这个跟之前讲的可重入锁 ReentrantLock 是一模一样的,结论是不是就是:

  • 读锁 他有他自己一套的共享锁逻辑,写锁 就走跟 ReentrantLock 的逻辑呢?

当然不是,这两个根本不是同一个东西,Sync 是各自的内部类大家都不一样,洒瓜!

有一个非常核心关键的东西我在前面一直都不提,就是为了在对比 ReentrantReadWriteLock 和 ReentrantLock 的时候想要提的,Sync。我认为它就是体现 AQS 是一个同步框架这个理念的核心。

Sync 是体现 AQS 框架的核心

这么 Sync 具体是个什么东西呢?

先从它的用法来看,我们可以看到 AQS 的各种实现类都会继承一个 Sync,然后再细化提供 公平锁 和 非公平锁 的实现逻辑。

每一个 AQS 的实现类 内部 都会写一个 Sync 去继承 AQS,然后在里面修改对应的核心方法比如:tryAcquire、tryRelease。赋予这个 Sync 具有这个子类的一些特性。

  • ReentrantReadWrite:共享和排他的特性。
  • ReentrantLock:可重入的特性。
  • .....

AQS 就是一个大的框架,定义好了同步的一些模版方法。 然后我们这些实现子类用一个内部类(Sync)去继承它,然后赋予它子类的特性。

State 在 ReentrantReadWriteLock 的定义

读锁写状态的存储

java 复制代码
    abstract static class Sync extends AbstractQueuedSynchronizer {
        private static final long serialVersionUID = 6317671515068378041L;

        /*
         * Read vs write count extraction constants and functions.
         * Lock state is logically divided into two unsigned shorts:
         * The lower one representing the exclusive (writer) lock hold count,
         * and the upper the shared (reader) hold count.
         */

        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; }

    }

对于 sharedCount,exclusiveCount 方法的参数 c 其实就是 AQS 里面的 state 变量。

注释里面已经很清晰地指出:

  • 锁的状态在逻辑上分为高 16 位和低 16 位

  • 高16位代表:读锁持有数

  • 低16位代表:写锁持有数

为什么会分成 16 位呢?

java 复制代码
    private volatile int state;

因为 state 变量定义是一个 int 类型,也就是 4 字节,32 位。

读写各占一半,所以是 16,这是可以不用死记硬背的。

读写锁次数的获取

那我们如何获取读锁和写锁的个数?

java 复制代码
    
    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;
    /** 获取读锁状态 */
    static int sharedCount(int c)    { return c >>> SHARED_SHIFT; }
    /** 获取写锁状态 */
    static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }
  • 读锁 :只需要通过 右移 16 位。

  • 写锁 :是 state 余上一个 EXCLUSIVE_MASK。

那 EXCLUSIVE_MASK 长什么样子呢,可以通过计算可以看出来他二进制就是:15个1。(这里是包含了读锁的哦,因为写锁读锁之间也会互斥)

到这里我们已经弄明白了 写锁和读锁的状态存储以及如何获取读锁和写锁的次数。

但是我们还没有弄清 读锁和写锁之间的 排他性和共享性 是如何实现的,下篇文章就剖析关于 写锁 的核心逻辑!

来都来了,点个赞,留个言再走吧彦祖👍,这对我来说非常重要!

相关推荐
Sayai1 小时前
MCP Server 开发实战:Spring AI 把后端日志查询暴露给 AI Agent(7 条设计原则与踩坑实录)
java·人工智能·ai agent·spring ai·mcp
大牧师1 小时前
Mybatis 框架教程
java·数据库·mybatis·数据持久化·orm
SunnyDays10111 小时前
Java 合并 PDF 文件:完整合并、指定页面合并与流合并
java·开发语言·合并pdf
金銀銅鐵2 小时前
[Java] 用GUI展示class文件顶层的 access_flags
后端·python·ai编程
源码宝2 小时前
云HIS系统源码,HIS全套源码,支持监管系统对接与二次开发
java·源码·电子病历·云his系统·程序代码·全套源码
萧瑟余晖2 小时前
Spring MVC拦截器异常处理与跨域详解
java·spring·mvc
YangYang9YangYan2 小时前
2027 届秋招求职|金融科技背景,反欺诈岗位面试案例题考点拆解
科技·面试·金融
泡茶喝茶写代码2 小时前
A股量化数据工程:从 REST 接口到策略信号(第 17 篇):创新高与新低扫描
java·python·股票数据api·股票数据api接口·股票api数据接口·股票量化数据api·股票量化数据接口
IT_陈寒2 小时前
Vue的数组更新把我坑惨了
前端·人工智能·后端