Skip to content

Locks-ReentrantReadWriteLock源码解析

一、RRWLock的设计目标

ReentrantLock排他锁(Exclusive Lock),同一时间只能有一个线程持有锁,适用于写多读少的场景。但在读多写少的场景(如缓存、数据库快照读),排他锁会导致大量读线程阻塞,降低并发性能。

RRWLock的设计目标是分离读写操作

  • 读锁(Shared Lock):多个读线程可同时持有,共享资源。
  • 写锁(Exclusive Lock):同一时间只能有一个写线程持有,排他性。

通过读写分离,RRWLock在读多写少场景下的性能远优于ReentrantLock

二、RRWLock的核心特性

1. 读写互斥

  • 写锁持有期间,所有读线程和其他写线程都无法获取锁;
  • 读锁持有期间,所有写线程无法获取锁,但其他读线程可以继续获取读锁。

2. 可重入性

  • 写线程:持有写锁的线程可以重入写锁(writeLock.lock()多次),也可以重入读锁(readLock.lock());
  • 读线程:持有读锁的线程可以重入读锁,但无法重入写锁(会导致死锁)。

3. 公平与非公平

  • 非公平模式(默认):线程获取锁时不遵循等待顺序,允许“插队”(读线程可插队到写线程前),性能更高,但可能导致写线程饥饿(长期无法获取锁);
  • 公平模式:线程按等待队列的顺序获取锁,避免饥饿,但性能稍差。

4. 锁降级

写线程可以降级为读线程(先获取写锁,再获取读锁,最后释放写锁),但读线程无法升级为写锁(会导致死锁)。

三、RRWLock的底层结构

RRWLock基于AQS(AbstractQueuedSynchronizer)实现,核心是Sync内部类(继承AQS),并分为 公平(FairSync)非公平(NonfairSync) 两个子类。

1. 状态拆分(State的复用)

AQS的state字段是32位int,RRWLock将其拆分为两部分:

  • 高16位:读状态(sharedCount),表示当前持有读锁的线程数量(注意:不是线程数,而是读锁的重入次数总和);
  • 低16位:写状态(exclusiveCount),表示当前写锁的重入次数。

位运算定义Sync类中的常量):

java
private static final int SHARED_SHIFT = 16;       // 读状态偏移量
private static final int SHARED_UNIT = (1 << SHARED_SHIFT);  // 读状态单位(1<<16=65536)
private static final int MAX_COUNT = (1 << SHARED_SHIFT) - 1; // 读写状态的最大值(65535)
private static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1; // 写状态掩码(0xffff)

// 获取读状态(高16位)
static int sharedCount(int c) { return c >>> SHARED_SHIFT; }
// 获取写状态(低16位)
static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }

例如,state=0x00020003

  • 读状态:0x0002(即2,代表读锁被重入2次);
  • 写状态:0x0003(即3,代表写锁被重入3次)。

2. 内部类结构

RRWLock有两个内部类实现Lock接口:

  • ReadLock:读锁,对应AQS的共享模式tryAcquireShared/tryReleaseShared);
  • WriteLock:写锁,对应AQS的排他模式tryAcquire/tryRelease)。
java
public class ReentrantReadWriteLock implements ReadWriteLock, java.io.Serializable {
    private final ReentrantReadWriteLock.ReadLock readerLock;  // 读锁
    private final ReentrantReadWriteLock.WriteLock writerLock;  // 写锁
    private final Sync sync;  // 核心同步器

    public ReentrantReadWriteLock() {
        this(false); // 默认非公平模式
    }

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

    // 读锁内部类
    public static class ReadLock implements Lock { /* ... */ }
    // 写锁内部类
    public static class WriteLock implements Lock { /* ... */ }
    // 同步器抽象类(继承AQS)
    abstract static class Sync extends AbstractQueuedSynchronizer { /* ... */ }
    // 非公平同步器
    static final class NonfairSync extends Sync { /* ... */ }
    // 公平同步器
    static final class FairSync extends Sync { /* ... */ }
}

四、源码深度剖析

1. 写锁(WriteLock)的实现

写锁是排他锁,其核心逻辑在SynctryAcquire(获取锁)和tryRelease(释放锁)方法中。

(1)写锁获取:tryAcquire

tryAcquire方法遵循以下逻辑:

  • state=0(无锁),尝试CAS将state设置为acquires(默认1),成功则标记当前线程为排他所有者
  • state≠0,检查是否是当前线程持有写锁exclusiveCount(state)≠0getExclusiveOwnerThread()==current),若是则重入(state+=acquires);
  • 否则,返回false(获取失败,进入等待队列)。

非公平模式的tryAcquire源码

java
protected final boolean tryAcquire(int acquires) {
    Thread current = Thread.currentThread();
    int c = getState();
    int w = exclusiveCount(c); // 获取写状态(低16位)
    if (c != 0) {
        // 情况1:state≠0(有锁持有)
        // 子情况1.1:写状态为0(说明是读锁持有),或当前线程不是写锁所有者 → 失败
        if (w == 0 || current != getExclusiveOwnerThread())
            return false;
        // 子情况1.2:写状态超过最大值(65535)→ 抛出错误
        if (w + acquires > MAX_COUNT)
            throw new Error("Maximum lock count exceeded");
        // 子情况1.3:重入写锁 → 更新state
        setState(c + acquires);
        return true;
    }
    // 情况2:state=0(无锁)→ 尝试CAS获取写锁
    // writerShouldBlock():非公平模式返回false(允许插队),公平模式返回hasQueuedPredecessors()
    if (writerShouldBlock() || !compareAndSetState(c, c + acquires))
        return false;
    // 成功:标记当前线程为排他所有者
    setExclusiveOwnerThread(current);
    return true;
}

关键细节

  • writerShouldBlock():非公平模式返回false(允许写线程插队),公平模式返回hasQueuedPredecessors()(检查等待队列是否有前驱节点,有则阻塞,遵循公平顺序);
  • 写锁的排他性:若有读锁持有(w=0c≠0),写线程无法获取锁,确保读写互斥。

(2)写锁释放:tryRelease

tryRelease方法逻辑简单:

  • state减去acquires(默认1);
  • state减到0,标记排他所有者null(完全释放);
  • 返回true当且仅当state减到0(表示写锁完全释放)。

源码

java
protected final boolean tryRelease(int acquires) {
    if (!isHeldExclusively()) // 当前线程不是写锁所有者 → 抛出异常
        throw new IllegalMonitorStateException();
    int nextc = getState() - acquires;
    boolean free = exclusiveCount(nextc) == 0; // 写状态是否为0
    if (free)
        setExclusiveOwnerThread(null); // 完全释放,清除所有者
    setState(nextc);
    return free;
}

2. 读锁(ReadLock)的实现

读锁是共享锁,其核心逻辑在SynctryAcquireShared(获取锁)和tryReleaseShared(释放锁)方法中。

(1)读锁获取:tryAcquireShared

读锁获取的逻辑更复杂,需要处理读共享可重入读写互斥

  • 若写锁被持有且不是当前线程exclusiveCount(c)≠0getExclusiveOwnerThread()≠current),返回-1(失败);
  • 若写锁未被持有或当前线程持有写锁(允许写线程重入读锁),尝试CAS增加读状态(state+=SHARED_UNIT);
  • 处理重入:记录当前线程的读锁重入次数(用ThreadLocal存储)。

源码

java
protected final int tryAcquireShared(int unused) {
    Thread current = Thread.currentThread();
    int c = getState();
    // 情况1:写锁被持有且不是当前线程 → 失败(返回-1)
    if (exclusiveCount(c) != 0 && getExclusiveOwnerThread() != current)
        return -1;
    int r = sharedCount(c); // 获取读状态(高16位)
    // 情况2:尝试获取读锁(非阻塞且读状态未超最大值)
    if (!readerShouldBlock() && r < MAX_COUNT && compareAndSetState(c, c + SHARED_UNIT)) {
        // 子情况2.1:第一次获取读锁(r=0)→ 记录firstReader(优化,避免ThreadLocal查找)
        if (r == 0) {
            firstReader = current;
            firstReaderHoldCount = 1;
        } else if (firstReader == current) {
            // 子情况2.2:firstReader重入 → 增加firstReaderHoldCount
            firstReaderHoldCount++;
        } else {
            // 子情况2.3:其他线程重入 → 从ThreadLocal获取HoldCounter
            HoldCounter rh = cachedHoldCounter;
            if (rh == null || rh.tid != getThreadId(current))
                cachedHoldCounter = rh = readHolds.get(); // readHolds是ThreadLocal<HoldCounter>
            else if (rh.count == 0)
                readHolds.set(rh);
            rh.count++; // 增加读锁次数
        }
        return 1; // 成功(返回正数)
    }
    // 情况3:CAS失败或需要阻塞 → 进入循环重试(fullTryAcquireShared)
    return fullTryAcquireShared(current);
}

关键细节

  • readerShouldBlock():非公平模式返回false(允许读线程插队),公平模式返回hasQueuedPredecessors()(遵循公平顺序);
  • firstReader优化:为了减少ThreadLocal的查找开销,RRWLock用firstReader记录第一个获取读锁的线程,其重入次数用firstReaderHoldCount记录(避免每次都查ThreadLocal);
  • HoldCounter:存储每个线程的读锁重入次数,用ThreadLocal<HoldCounter>readHolds)实现:
    java
    static final class HoldCounter {
        int count = 0;
        final long tid = getThreadId(Thread.currentThread()); // 线程ID
    }
    private transient ThreadLocal<HoldCounter> readHolds = new ThreadLocal<>();
    private transient HoldCounter cachedHoldCounter; // 缓存最近的HoldCounter,优化查找

(2)读锁释放:tryReleaseShared

读锁释放需要处理重入次数减少读状态更新

  • 减少当前线程的读锁重入次数(更新firstReaderHoldCountHoldCounter);
  • 循环CAS减少读状态(state-=SHARED_UNIT);
  • 返回true当且仅当state减到0(表示所有读锁都释放)。

源码

java
protected final boolean tryReleaseShared(int unused) {
    Thread current = Thread.currentThread();
    // 情况1:当前线程是firstReader → 更新firstReaderHoldCount
    if (firstReader == current) {
        if (firstReaderHoldCount == 1)
            firstReader = null; // 完全释放,清除firstReader
        else
            firstReaderHoldCount--;
    } else {
        // 情况2:其他线程 → 从ThreadLocal获取HoldCounter
        HoldCounter rh = cachedHoldCounter;
        if (rh == null || rh.tid != getThreadId(current))
            rh = readHolds.get();
        int count = rh.count;
        if (count <= 1) {
            readHolds.remove(); // 完全释放,移除ThreadLocal中的记录
            if (count <= 0)
                throw new IllegalMonitorStateException(); // 释放次数超过获取次数
        }
        rh.count--; // 减少读锁次数
    }
    // 情况3:循环CAS更新读状态(避免并发修改)
    for (;;) {
        int c = getState();
        int nextc = c - SHARED_UNIT;
        if (compareAndSetState(c, nextc))
            return nextc == 0; // 返回true当且仅当所有锁都释放
    }
}

3. 公平与非公平的区别

公平与非公平的核心区别在于**writerShouldBlock()readerShouldBlock()**方法的实现:

  • 非公平模式(NonfairSync)
    java
    final boolean writerShouldBlock() {
        return false; // 写线程不阻塞,允许插队
    }
    final boolean readerShouldBlock() {
        return false; // 读线程不阻塞,允许插队
    }
  • 公平模式(FairSync)
    java
    final boolean writerShouldBlock() {
        return hasQueuedPredecessors(); // 写线程检查等待队列是否有前驱节点,有则阻塞
    }
    final boolean readerShouldBlock() {
        return hasQueuedPredecessors(); // 读线程检查等待队列是否有前驱节点,有则阻塞
    }

hasQueuedPredecessors():判断等待队列中是否有比当前线程更早等待的线程(即队列头部是否是当前线程),若有则返回true(需要阻塞),否则返回false(可以获取锁)。

五、关键细节与注意事项

1. 读锁无法升级为写锁

若一个线程持有读锁,再尝试获取写锁,会导致死锁

java
ReadWriteLock rwLock = new ReentrantReadWriteLock();
Lock readLock = rwLock.readLock();
Lock writeLock = rwLock.writeLock();

// 线程A的操作
readLock.lock();
try {
    // 尝试获取写锁(读锁未释放)→ 阻塞
    writeLock.lock();
    // 永远无法执行到这里
} finally {
    readLock.unlock();
}

原因:线程A持有读锁,尝试获取写锁时,需要等待所有读锁释放(包括自己的),但自己的读锁未释放,导致死锁。

2. 写锁可以降级为读锁

写线程可以安全地将写锁降级为读锁(先获取写锁,再获取读锁,最后释放写锁):

java
ReadWriteLock rwLock = new ReentrantReadWriteLock();
Lock readLock = rwLock.readLock();
Lock writeLock = rwLock.writeLock();

// 线程A的操作
writeLock.lock();
try {
    // 执行写操作(修改数据)
    updateData();
    // 获取读锁(降级)
    readLock.lock();
} finally {
    writeLock.unlock(); // 释放写锁,保留读锁
}

try {
    // 执行读操作(读取修改后的数据)
    readData();
} finally {
    readLock.unlock(); // 释放读锁
}

好处:降级后,其他读线程可以共享数据,而当前线程仍持有读锁,确保数据一致性。

3. 写线程饥饿问题(非公平模式)

在非公平模式下,若有大量读线程持续获取读锁,写线程可能长期无法获取锁(饥饿)。例如:

  • 写线程W在等待队列中;
  • 读线程R1、R2、R3依次获取读锁,释放后,新的读线程R4又插队获取读锁;
  • 写线程W一直无法获取锁。

解决方法:使用公平模式ReentrantReadWriteLock(true)),确保线程按等待顺序获取锁,避免饥饿。

4. 锁的中断性

RRWLock的读锁和写锁都支持中断lockInterruptibly()方法),即线程在等待锁时可以响应中断,抛出InterruptedException

六、使用场景

RRWLock适用于读多写少的场景,例如:

  • 缓存系统:缓存更新(写锁)、缓存读取(读锁);
  • 数据库快照读:事务中的快照读(读锁)、数据修改(写锁);
  • 配置文件加载:配置文件更新(写锁)、配置读取(读锁)。

七、总结

ReentrantReadWriteLock读写分离的并发工具,核心思想是通过AQS的state字段拆分实现读写状态的管理,支持读共享、写排他,提高读多写少场景下的并发性能。其关键特性包括:

  • 读写互斥:确保写操作的原子性;
  • 可重入性:允许线程重入读写锁;
  • 公平与非公平:平衡性能与饥饿问题;
  • 锁降级:支持写锁降级为读锁,确保数据一致性。

在使用时,需要注意读锁无法升级为写锁非公平模式下的写线程饥饿等问题,根据场景选择合适的公平策略。

参考资料

  • JDK 1.8源码(java.util.concurrent.locks.ReentrantReadWriteLock);
  • 《Java并发编程的艺术》(方腾飞等);
  • 《深入理解Java虚拟机》(周志明)。