跳转到正文
Siolin'Log
返回

Java ReadWriteLock:共享读、独占写与锁降级

概述

ReadWriteLock 的共享读与独占写关系

ReadWriteLock(读写锁)适用于“读多写少”的共享数据场景。它把普通互斥锁拆成两种锁:

这里的并发读成立于没有其他线程持有写锁,并且当前实现的排队策略允许获取读锁的前提下。

Java 中最常用的实现是 ReentrantReadWriteLock,内部提供两把逻辑锁:

Lock readLock();
Lock writeLock();

为什么需要读写锁

使用普通互斥锁时:

private final Lock lock = new ReentrantLock();

public String get(String key) {
    lock.lock();
    try {
        return map.get(key);
    } finally {
        lock.unlock();
    }
}

即使两个线程都只是读取数据,也不能同时执行:

线程 A 读取 ──────────────
线程 B                等待 ──── 读取

但如果读取操作不会修改共享数据,多个读线程通常可以安全并发:

线程 A 读取 ──────────────
线程 B 读取 ──────────────
线程 C 读取 ──────────────

只有写操作需要排他执行:

线程 A 读取 ─────
线程 B 读取 ─────
                线程 C 写入 ─────

因此,读写锁把访问分成两种模式:

接口和实现

ReadWriteLock 接口

接口非常简单:它只负责提供两把关联的锁。

public interface ReadWriteLock {

    Lock readLock();

    Lock writeLock();
}

返回的是普通 Lock 接口,因此读锁和写锁都可以调用:

lock()
lockInterruptibly()
tryLock()
tryLock(timeout, unit)
unlock()

不过具体实现仍可限制某些操作。例如 ReentrantReadWriteLock 的写锁支持 newCondition(),读锁调用 newCondition() 则会抛出 UnsupportedOperationException

ReentrantReadWriteLock

Java 最常用的读写锁实现:ReentrantReadWriteLock

ReentrantReadWriteLock rwLock =
        new ReentrantReadWriteLock();

Lock readLock = rwLock.readLock();
Lock writeLock = rwLock.writeLock();

也可以配置公平策略:

new ReentrantReadWriteLock(true);

默认是非公平模式:

new ReentrantReadWriteLock(false);

锁降级

ReentrantReadWriteLock 支持从写锁降级为读锁。

所谓锁降级,是指:

持有写锁

在释放写锁之前获得读锁

释放写锁

继续持有读锁

代码结构:

writeLock.lock();

try {
    updateSharedState();

    readLock.lock();
} finally {
    writeLock.unlock();
}

try {
    readSharedState();
} finally {
    readLock.unlock();
}

锁降级的意义是:线程完成修改以后,仍然可以继续稳定地读取修改后的数据,同时允许其他读线程进入

如果先释放写锁,再获取读锁:

writeLock.unlock();
readLock.lock();

两次操作之间存在空窗期,其他写线程可能抢先修改数据。

反方向的“锁升级”不受支持:线程持有读锁时再等待写锁不会成功,必须先释放读锁,再单独竞争写锁,并在获取后重新检查共享状态。

底层 state

ReentrantReadWriteLock 基于 Java AQS,但 AQS 只有一个:

volatile int state;

读写锁需要同时记录:

因此,它把 32 位 int 拆成两部分:

高 16 位:读锁总计数
低 16 位:写锁重入次数

示意:

31                       16 15                        0
┌──────────────────────────┬──────────────────────────┐
│       读锁计数            │       写锁计数            │
└──────────────────────────┴──────────────────────────┘

获取读锁计数:

int readCount = state >>> 16;

获取写锁计数:

int writeCount = state & 0xFFFF;

两个计数各占 16 位,因此单个计数的最大值都是 65535;超过限制时实现会抛出错误,而不是继续溢出。



上一篇
Java 并发问题:原子性、可见性与有序性
下一篇
xv6 Trap 机制