Skip to content

ReentrantReadWriteLock详解

ReentrantReadWriteLock 是 Java 提供的可重入读写锁。它适合“读多写少”的场景,例如配置缓存、商品详情缓存、字典数据缓存。

如果要从零理解它的完整原理、AQS 状态拆分、读锁写锁获取流程、锁降级、线上排查和面试回答,建议继续看:读写锁全过程原理

零基础可以先这样理解:

读锁允许多个线程一起读;写锁要求独占,写的时候不能有人读,也不能有人写。

如果只用普通互斥锁,多个读操作也会互相阻塞,读多写少时性能会浪费。读写锁把访问分成读和写两类,让安全的并发读可以同时进行。

为什么读写锁只适合读多写少

读写锁的收益来自“多个读线程可以同时进入”。如果写操作很频繁,读锁和写锁会频繁互斥,维护读写状态、排队和唤醒的成本反而可能比普通互斥锁更高。

mermaid
flowchart TD
    A["读多写少"] --> B["多个读线程共享读锁"]
    B --> C["写线程偶尔独占"]
    C --> D["整体吞吐提升"]
    E["写多或读写都多"] --> F["读写频繁互相阻塞"]
    F --> G["读写锁收益下降"]

工作原理图

mermaid
flowchart TD
    A["线程请求锁"] --> B{"请求读锁还是写锁"}
    B -- "读锁" --> C{"当前是否有写锁持有"}
    C -- "没有" --> D["读计数 + 1\n允许多个读线程同时进入"]
    C -- "有" --> E["进入 AQS 队列等待"]
    B -- "写锁" --> F{"当前是否无人读且无人写"}
    F -- "是" --> G["写计数 + 1\n写线程独占进入"]
    F -- "否" --> E
    D --> H["读完释放读锁\n读计数 - 1"]
    G --> I["写完释放写锁\n写计数 - 1"]
    H --> J{"是否可以唤醒队列线程"}
    I --> J
    J -- "可以" --> K["唤醒后继线程继续竞争"]

AQS 的 state 是一个 int,读写锁把它拆成两半使用:高 16 位记录读锁次数,低 16 位记录写锁重入次数。因此它能同时管理“有多少读线程”和“写线程重入了几次”。

类结构分析

ReentrantReadWriteLock是ReadWriteLock(读写锁)的实现类。ReadWriteLock接口如下:

java
public interface ReadWriteLock {
    /**
     * Returns the lock used for reading.
     *
     * @return the lock used for reading
     */
    Lock readLock();
    /**
     * Returns the lock used for writing.
     *
     * @return the lock used for writing
     */
    Lock writeLock();
}

构造方法

ReentrantReadWriteLock的无参构造默认创建非公平的同步机制。方法如下:

java
public ReentrantReadWriteLock() {
    this(false);
}
/**
 * Creates a new {@code ReentrantReadWriteLock} with
 * the given fairness policy.
 *
 * @param fair {@code true} if this lock should use a fair ordering policy
 */
public ReentrantReadWriteLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
    readerLock = new ReadLock(this);
    writerLock = new WriteLock(this);
}

其中FairSync和NonfairSync都是Sync的子类,一个是公平的一个是非公平的。

java
    /**
     * Nonfair version of Sync
     */
    static final class NonfairSync extends Sync {
        private static final long serialVersionUID = -8159625535654395037L;
        final boolean writerShouldBlock() {
            return false; // writers can always barge
        }
        final boolean readerShouldBlock() {
            return apparentlyFirstQueuedIsExclusive();
        }
    }
    /**
     * Fair version of Sync
     */
    static final class FairSync extends Sync {
        private static final long serialVersionUID = -2274990926593161451L;
        final boolean writerShouldBlock() {
            return hasQueuedPredecessors();
        }
        final boolean readerShouldBlock() {
            return hasQueuedPredecessors();
        }
    }

和ReentrantLock一样,有一个Sync的静态内部类也是继承了AbstractQueuedSynchronizer但实现不同。另外还有ReadLock和WriteLock这两个比较重要的内部类。 ReadLock和WriteLock都实现了Lock接口

使用 Demo:读多写少缓存

java
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class ConfigCache {
    private final Map<String, String> cache = new HashMap<>();
    private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();

    public String get(String key) {
        rwLock.readLock().lock();
        try {
            return cache.get(key);
        } finally {
            rwLock.readLock().unlock();
        }
    }

    public void put(String key, String value) {
        rwLock.writeLock().lock();
        try {
            cache.put(key, value);
        } finally {
            rwLock.writeLock().unlock();
        }
    }
}

这个例子里,多个线程同时 get() 不会互相阻塞;只要有线程 put(),写锁会独占,避免读到写入一半的数据。

读锁和写锁的关系

场景是否允许
多个线程同时读允许
一个线程写,其他线程读不允许
一个线程写,其他线程写不允许
持有写锁的线程再获取写锁允许,可重入
持有写锁的线程再获取读锁允许,叫锁降级
持有读锁后升级成写锁不建议,容易死锁

锁降级示例:

java
rwLock.writeLock().lock();
try {
    // 修改缓存
    cache.put("mode", "new");

    // 写锁未释放前先拿读锁,保证后续读取的是自己刚写入的数据。
    rwLock.readLock().lock();
} finally {
    rwLock.writeLock().unlock();
}

try {
    System.out.println(cache.get("mode"));
} finally {
    rwLock.readLock().unlock();
}

使用读写锁时要先确认业务确实是读多写少。如果写操作很频繁,读线程会经常被写锁阻塞,收益就不明显。

常见风险

风险后果正确做法
持有读锁时直接申请写锁可能死锁,读锁不释放写锁拿不到不做锁升级,先释放读锁再竞争写锁
读操作里修改共享对象多个读线程同时修改导致数据错乱读锁内只读不可变或稳定状态
写锁持有时间过长所有读线程被阻塞写锁内只做必要修改
忘记 finally 解锁锁永远不释放,线程全部卡住lock() 后必须 try/finally unlock()