并发工具类:ReentrantReadWriteLock是如何做到读读并行的?

本文详细解析了ReadWriteLock的特点,包括读写锁的工作原理、公平与非公平锁的区别,以及读写操作的互斥与并行性。重点介绍了写锁的获取与释放,以及读锁的获取策略,以及支持的锁升级与降级情况。

在这里插入图片描述

ReentrantReadWriteLock的特点

普通的锁(如 ReentrantLock 或 synchronized)都是独占锁(排他锁),即无论读写,同一时刻只允许一个线程访问。但在很多实际场景中,读操作远多于写操作。如果读读也互斥,就会极大限制并发性能

ReentrantReadWriteLock 通过将锁拆分为读锁(ReadLock) 和 写锁(WriteLock),实现了“读读共享,读写互斥,写写互斥”,在读多写少的场景下能够大幅提升并发吞吐量

在这里插入图片描述

和ReentrantLock类似ReadWriteLock也分为公平锁和非公平锁

public ReentrantReadWriteLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
    readerLock = new ReadLock(this);
    writerLock = new WriteLock(this);
}

ReentrantReadWriteLock 维护了一对锁

读锁:允许多个线程同时获取,进行并发读取

写锁:同一时刻只允许一个线程获取,进行独占写入

锁状态 / 尝试获取读锁写锁
无锁允许允许
已加读锁允许(读读共享)拒绝(读写互斥)
已加写锁拒绝(读写互斥)拒绝(写写互斥)

从ReadWriteLock的行为我们可以猜到,写锁是互斥锁,读锁是共享锁,但是AQS中只提供了一个state变量来表示锁的状态。

我们如何用一个变量来存储两种锁的状态呢?

在ReadWriteLock中是这样做的,state变量的高16位表示读锁的状态,低16位表示写锁的状态
在这里插入图片描述

核心特性

可重入性

读锁和写锁都支持重入。同一个线程在持有锁的情况下,可以再次获取该锁而不会发生死锁(底层通过内部计数器记录重入次数)

锁降级(Lock Downgrading)与锁升级(Lock Upgrading)

支持锁降级:持有写锁的线程,可以先获取读锁,再释放写锁。这样该线程就从“写模式”平滑降级为“读模式”,且在这个过程中能保证数据一致性(防止其他线程在降级瞬间修改数据)

不支持锁升级:持有读锁的线程不能直接获取写锁。如果需要获取写锁,必须先释放读锁。若多个线程同时持有读锁并尝试升级为写锁,会导致死锁

公平与非公平策略

非公平模式(默认):吞吐量更高,但可能导致写线程“饥饿”(因为读线程源源不断占锁)

公平模式:按照 FIFO(先进先出)队列顺序获取锁。如果队列中有线程在等待写锁,后续尝试获取读锁的线程也会被阻塞,从而避免写线程饥饿

获取写锁

鉴于写锁的实现比较简单,我们就先看写锁的实现,再看读锁的实现

// WriteLock
public void lock() {
    sync.acquire(1);
}
// AQS
public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

上面的代码我们在AQS中已经分析过了,不再分析了,直接分析加锁的逻辑

// Sync
protected final boolean tryAcquire(int acquires) {
    Thread current = Thread.currentThread();
    int c = getState();
    // 获取写锁的值
    int w = exclusiveCount(c);
    if (c != 0) {
        // state不为0,写锁为0,说明读锁不为0
        // (Note: if c != 0 and w == 0 then shared count != 0)
        // 1. 读锁不为0
        // 2. 写锁不为0,并且获取写锁的线程不是当前线程,则写锁加锁失败
        if (w == 0 || current != getExclusiveOwnerThread())
            return false;
        // 超过写锁能表示的最大获取次数
        if (w + exclusiveCount(acquires) > MAX_COUNT)
            throw new Error("Maximum lock count exceeded");
        // Reentrant acquire
        // 写锁重入
        setState(c + acquires);
        return true;
    }
    // 没有被加锁,先看看是否需要排队
    if (writerShouldBlock() ||
        !compareAndSetState(c, c + acquires))
        return false;
    // 获锁成功,执行业务逻辑
    setExclusiveOwnerThread(current);
    return true;
}

从上面的源码中我们可以看到申请写锁的时候,只要有读锁就会失败,因此ReadWriteLock并不支持锁升级

加锁时公平锁和非公平锁的逻辑和ReentrantLock一样

static final class NonfairSync extends Sync {
    // 非公平模式,直接cas去抢锁,抢不到再排队
    final boolean writerShouldBlock() {
        return false; // writers can always barge
    }
}

static final class FairSync extends Sync {
    // 同步队列中有线程则去排队
    final boolean writerShouldBlock() {
        return hasQueuedPredecessors();
    }
}

释放写锁

// WriteLock
public void unlock() {
    sync.release(1);
}
// AQS
public final boolean release(int arg) {
    if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}

直接看释放锁的逻辑

// Sync
protected final boolean tryRelease(int releases) {
    // 解锁的线程和获取锁的线程不一样
    if (!isHeldExclusively())
        throw new IllegalMonitorStateException();
    int nextc = getState() - releases;
    // 写锁是可重入的,判断所有的写锁是否都被释放
    boolean free = exclusiveCount(nextc) == 0;
    if (free)
        setExclusiveOwnerThread(null);
    setState(nextc);
    return free;
}

将写锁的加锁次数减一,因为写锁是可重入的。当写锁都被释放时,唤醒同步队列中的线程,否则只是修改次数

获取读锁

// ReadLock
public void lock() {
    sync.acquireShared(1);
}
// AQS
public final void acquireShared(int arg) {
    if (tryAcquireShared(arg) < 0)
        doAcquireShared(arg);
}

直接看加锁的逻辑

// Sync
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);
    // 是否需要排队
    // 是否超过能表示的加锁次数
    // cas加锁
    if (!readerShouldBlock() &&
        r < MAX_COUNT &&
        compareAndSetState(c, c + SHARED_UNIT)) {
        if (r == 0) {
            // 第一个获取读锁
            firstReader = current;
            firstReaderHoldCount = 1;
        } else if (firstReader == current) {
            // 读锁重入
            firstReaderHoldCount++;
        } else {
            // cachedHoldCounter用来保存最后一个获取读锁的线程
            HoldCounter rh = cachedHoldCounter;
            if (rh == null || rh.tid != getThreadId(current))
                cachedHoldCounter = rh = readHolds.get();
            else if (rh.count == 0)
                readHolds.set(rh);
            rh.count++;
        }
        // 从 AQS中acquireShared方法可以知道大于0表示获取到锁
        return 1;
    }
    // 自旋获取读锁
    return fullTryAcquireShared(current);
}

当我们加读锁的时候,如果有写锁并且不是当前线程就会加锁失败。如果有写锁并且是当前线程那么可以正常获取读锁,因此ReadWriteLock是支持锁降级的

firstReader,cachedHoldCounter等只是一些统计变量,例如读锁的获取次数,对主流程影响不大,不展开分析了

释放读锁

// ReadLock
public void unlock() {
    sync.releaseShared(1);
}
// AQS
public final boolean releaseShared(int arg) {
    if (tryReleaseShared(arg)) {
        doReleaseShared();
        return true;
    }
    return false;
}
// Sync
protected final boolean tryReleaseShared(int unused) {

    // 省略部分无关代码
    
    for (;;) {
        int c = getState();
        // 将读锁次数减1
        int nextc = c - SHARED_UNIT;
        if (compareAndSetState(c, nextc))
            // nextc == 0表示读锁和写锁都被释放了
            return nextc == 0;
    }
}

通过CAS不断减少读锁的加锁次数。

总结

读取是获取共享锁,在获取读锁之前会先判断写锁是否被获取,如果写锁被当前线程获取或者没有写锁,则获取读锁成功,否则获取读锁失败(支持锁降级)

写锁是获取独占锁,在获取之前会先判断读锁是否被获取,如果读锁已经被获取,则获取写锁失败。如果写锁没有被获取或者已经被当前线程获取,则获取写锁成功,否则获取写锁失败

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Java识堂

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值