原来这叫读写锁 —— ReentrantReadWriteLock

大家好,我是徒手敲代码。

今天来介绍一下java.util.concurrent.locks包下的ReentrantReadWriteLock

顾名思义,它是读写锁的一种,同一时间,读操作可以有多个线程,但是写操作只能有一个线程,并且读和写不能同时进行;读锁被占用,那么写锁就不能获取,反过来也一样。

之前学过的互斥锁,比如ReentrantLocksynchronized,在任何时候都只允许一个线程访问共享资源,这在读操作远多于写操作的场景下,显得效率很低,因为即使多个读操作之间并不冲突,它们也必须排队等待。

ReentrantReadWriteLock的诞生,正好可以解决这个问题。它通过分离读锁和写锁,使得并发读成为可能。

下面通过阅读源码的方式,来看看大佬是如何设计这个读写锁的。

读写状态的设计

我们知道,基于 AQS 的锁实现,内部都是通过一个int类型的state变量,来维护锁的状态。Java 中,int有 32 位,而ReentrantReadWriteLock就是利用这些位来分别表示读锁和写锁的持有情况,这种设计被称为按位切割使用

读写锁将变量切分为两个部分,高16位表示读,低16位表示写,通过位运算来快速确定,读和写各自的状态。

写锁的获取和释放

写锁是一个支持重进入的排它锁。如果当前线程已经获取了写锁,则增加写状态;

如果当前线程在获取写锁时,读锁已经被获取(读状态不为0)或者该线程不是已经获取写锁的线程, 则当前线程进入等待状态。

tryAcquire这个方法:

java 复制代码
protected final boolean tryAcquire(int acquires) {
    Thread current = Thread.currentThread();
    int c = getState();
    int w = exclusiveCount(c);
    if (c != 0) {
        //存在读锁,或者当前线程不是已经获取写锁的线程
        if (w == 0 || current != getExclusiveOwnerThread())
            return false;
        if (w + exclusiveCount(acquires) > MAX_COUNT)
            throw new Error("Maximum lock count exceeded");
        setState(c + acquires);
        return true;
    }
    if (writerShouldBlock() ||
        !compareAndSetState(c, c + acquires))
        return false;
    setExclusiveOwnerThread(current);
    return true;
}

这里之所以要判断是否存在读锁,因为读写锁要确保写锁的操作对读锁可见。

如果允许读锁在已被获取的情况下,还获取写锁,那么正在运行的其他读线程,就没办法感知到当前写线程的操作。因此,只有等待其他读线程都释放了读锁,写锁才能被当前线程获取,而写锁一旦被获取,则其他读写线程的后续访问都会被阻塞。

至于写锁的释放,跟ReentrantLock的释放过程基本类似,每次释放都会减少写状态,当写状态为0 时,表示写锁已被释放,等待的读写线程能够继续访问读写锁,同时之前写线程的修改,对后续读写线程可见。

读锁的获取

读锁的获取通过tryAcquireShared()方法执行,与写锁不同,只要没有写锁被持有,就可以允许多个读锁同时存在。

该方法会检查当前状态,确保没有写锁且读锁计数未达到最大限制(防止整型溢出)。成功获取后,会增加读锁的重入计数。

读锁的获取方法,代码如下:

ini 复制代码
protected final int tryAcquireShared(int unused) {
    Thread current = Thread.currentThread();
    int c = getState();
    if (exclusiveCount(c) != 0 &&
        getExclusiveOwnerThread() != current)
        return -1;
    int r = sharedCount(c);
    if (!readerShouldBlock() &&
        r < MAX_COUNT &&
        compareAndSetState(c, c + SHARED_UNIT)) {
        if (r == 0) {
            firstReader = current;
            firstReaderHoldCount = 1;
        } else if (firstReader == current) {
            firstReaderHoldCount++;
        } else {
            HoldCounter rh = cachedHoldCounter;
            if (rh == null || rh.tid != getThreadId(current))
                cachedHoldCounter = rh = readHolds.get();
            else if (rh.count == 0)
                readHolds.set(rh);
                rh.count++;
            }
        return 1;
    }
    return fullTryAcquireShared(current);
}

这个方法有个很奇怪的点,入参并没有被使用,资料说是占位符。

获取读锁的主要逻辑:如果其他线程已经获取了写锁,则当前线程获取读锁失败,进入等待状态;如果当前线程获取了写锁,或者写锁未被获取,则当前线程增加读状态(CAS 操作),成功获取读锁。

读锁的释放

直接看tryReleaseShared方法的代码:

ini 复制代码
protected final boolean tryReleaseShared(int unused) {
    Thread current = Thread.currentThread();
    if (firstReader == current) {
        if (firstReaderHoldCount == 1)
            firstReader = null;
        else
            firstReaderHoldCount--;
    } else {
        HoldCounter rh = cachedHoldCounter;
        if (rh == null || rh.tid != getThreadId(current))
            rh = readHolds.get();
        int count = rh.count;
        if (count <= 1) {
            readHolds.remove();
            if (count <= 0)
                throw unmatchedUnlockException();
        }
        --rh.count;
    }
    for (;;) {
        int c = getState();
        int nextc = c - SHARED_UNIT;
        if (compareAndSetState(c, nextc))
            return nextc == 0;
    }
}

根据当前线程是否首次获取读锁,分别减少读锁的获取计数;然后通过 CAS 操作来释放读锁,因为可能会有多个线程同时释放读锁。

今天的分享到这里结束了。

关注公众号"徒手敲代码",免费领取腾讯大佬推荐的Java电子书!

相关推荐
程序员爱钓鱼16 分钟前
Go语言实战案例 — 工具开发篇:实现一个图片批量压缩工具
后端·google·go
程序员的世界你不懂1 小时前
【Flask】测试平台开发,新增说明书编写和展示功能 第二十三篇
java·前端·数据库
星空寻流年1 小时前
设计模式第一章(建造者模式)
java·设计模式·建造者模式
在未来等你1 小时前
Kafka面试精讲 Day 13:故障检测与自动恢复
大数据·分布式·面试·kafka·消息队列
gb42152872 小时前
java中将租户ID包装为JSQLParser的StringValue表达式对象,JSQLParser指的是?
java·开发语言·python
曾经的三心草2 小时前
Python2-工具安装使用-anaconda-jupyter-PyCharm-Matplotlib
android·java·服务器
Metaphor6922 小时前
Java 高效处理 Word 文档:查找并替换文本的全面指南
java·经验分享·word
ChinaRainbowSea2 小时前
7. LangChain4j + 记忆缓存详细说明
java·数据库·redis·后端·缓存·langchain·ai编程
舒一笑2 小时前
同步框架与底层消费机制解决方案梳理
后端·程序员
stormsha2 小时前
飞算JavaAI炫技赛电商系统商品管理模块的架构设计与实现
java·架构·鸿蒙系统