源码级丨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。(这里是包含了读锁的哦,因为写锁读锁之间也会互斥)

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

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

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

相关推荐
用户8356290780512 分钟前
使用 Python 在 Excel 图表中添加趋势线与回归方程
后端·python
你三大爷13 分钟前
Netty / Kafka / Redis 保序问题的源码对比与 NIO 框架主流解法
后端·架构
SL_staff13 分钟前
从配置失效到逻辑复用:JVS-Logic 如何构建可治理的业务编排中枢
java·spring·产品
特立独行的猫A15 分钟前
一份 Markdown 发十个平台:cspub 一稿多投
java·后端·python
3DGISer22 分钟前
不透明物体的渲染算法
后端
ServBay1 小时前
从代码到上线,Cursor Live Apps 能替独立开发者做什么
后端·ai编程·cursor
程序员cxuan1 小时前
LobeHub 是真的夯,简直 Super-Agent 最佳实现。
人工智能·后端·程序员
文丑颜不良啊1 小时前
LeetCode1961~1980题目解析
java·leetcode
文丑颜不良啊2 小时前
LeetCode2001~2020题目解析
java·leetcode
用户3721574261352 小时前
Java 批量提取 Word 文档中的图片并保存到本地
java