原来这叫读写锁 —— ReentrantReadWriteLock

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

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

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

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

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电子书!

相关推荐
希洛兔3 小时前
里氏替换原则详解:子类必须能透明替换基类(含继承风险演示 + 正确重构 + JDK 案例)
java·里氏替换原则
tachibana23 小时前
Go 语言的 Map 是否是并发安全的?
开发语言·数据结构·后端·golang·go·语言
dd聊技术3 小时前
发布前的那十几秒:AI 预检为什么不用「主编 Agent」
后端
kevien_G13 小时前
微服务之OpenFeign最佳实践
java·微服务·架构
Bazingga3 小时前
LLM记忆实战:从一次164秒慢问,到上下文治理与记忆版图
后端
江湖有缘3 小时前
Docker实战 | 使用Docker部署ShipShipShip自托管产品更新与功能投票系统
java·linux·服务器·docker
范什么特西3 小时前
亲和路由11
java
看浪的路人3 小时前
第8讲:RAG 生产化——Go 实现亿级向量召回 + 混合检索 + 重排序 Pipeline
开发语言·后端·golang
tachibana23 小时前
Go 语言 Map 的扩容过程是怎样的?
开发语言·后端·golang·go·语言
念何架构之路3 小时前
go-grpc服务端调用链
开发语言·后端·golang