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

源码篇 预防针💉:

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

前言

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

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

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

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

本篇着重从 读写锁的状态存储 进行介绍。中间会介绍在 AQSSync 到底扮演了一个什么角色?为什么说 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 是各自的内部类大家都不一样,洒瓜!

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

Sync 是体现 AQS 框架的核心

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

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

每一个 AQS 的实现类 内部 都会写一个 Sync 去继承 AQS,然后在里面修改对应的核心方法比如:tryAcquiretryRelease。赋予这个 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; }

    }

对于 sharedCountexclusiveCount 方法的参数 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。(这里是包含了读锁的哦,因为写锁读锁之间也会互斥)

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

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

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

相关推荐
Ruihong几秒前
你的 Vue 3 defineProps(),VuReact 会编译成什么样的 React?
vue.js·react.js·面试
前端摸鱼匠1 分钟前
【AI大模型春招面试题21】什么是Transformer的“预归一化”与“后归一化”?两者的差异及影响?
人工智能·深度学习·面试·大模型·transformer·求职招聘
无巧不成书02185 分钟前
Java异常体系与处理全解:核心原理、实战用法、避坑指南
java·开发语言·异常处理·java异常处理体系
8Qi87 分钟前
RabbitMQ高级篇:消息可靠性、幂等性与延迟消息
java·分布式·微服务·中间件·rabbitmq·springcloud
yxl_num15 分钟前
Docker 完整部署一个包含 Spring Boot(依赖 JDK)、MySQL、Redis、Nginx 的整套服务
java·spring boot·docker
大鹏说大话23 分钟前
Go语言Channel并发编程实战:从基础通信到高级模式
开发语言·后端·golang
Jacky-00823 分钟前
Rust安装(MinGw64编译器安装)
开发语言·后端·rust
好家伙VCC25 分钟前
**发散创新:基于Python的自动化恢复演练框架设计与实战**在现代软件系统运维中,
java·开发语言·python·自动化
程序员小崔日记27 分钟前
我参加了第十七届蓝桥杯 Java B 组省赛,这套题你能撑到第几题?
java·算法·蓝桥杯大赛
大黄说说30 分钟前
Go并发双雄:WaitGroup与Channel的抉择与协作
java·服务器·数据库