Java并发编程——可重入读写锁(ReentrantReadWriteLock)源码解析

在Java并发编程中,锁是解决线程安全问题的核心工具,而 ReentrantReadWriteLock(可重入读写锁) 作为Lock接口的重要实现,凭借“读写分离”的特性,在高并发读多写少场景中性能远超synchronizedReentrantLock(排他锁)。
它既能保证多线程读操作的并发安全性,又能兼顾写操作的原子性,同时支持重入特性,极大提升了并发程序的灵活性和性能。

一、ReentrantReadWriteLock 核心特性

在深入源码前,我们先明确ReentrantReadWriteLock的核心特性,这是理解源码设计的基础,也是它区别于其他锁的关键:

  1. 读写分离:维护两把锁——读锁(共享锁)和写锁(排他锁)。多线程可同时获取读锁(并发读),但同一时刻只能有一个线程获取写锁(排他写);写锁持有期间,所有读线程和其他写线程均会被阻塞。

  2. 可重入性:读锁和写锁均支持重入。读线程获取读锁后,可再次获取读锁;写线程获取写锁后,可再次获取写锁,也可获取读锁(写锁降级为读锁)。

  3. 公平/非公平模式:支持两种锁获取模式,默认是非公平模式(性能更优),可通过构造函数指定为公平模式。公平模式下,线程按等待队列顺序获取锁;非公平模式下,线程可能插队获取锁。

  4. 锁降级:支持写锁降级为读锁(写线程持有写锁时,可获取读锁,再释放写锁,最终持有读锁),但不支持读锁升级为写锁(避免死锁)。

  5. 可中断性:支持锁获取的中断(lockInterruptibly()方法),线程在等待锁的过程中可被中断,避免无限等待。

补充:ReentrantReadWriteLock实现了 ReadWriteLock 接口,该接口仅定义了两个核心方法:readLock() 获取读锁,writeLock() 获取写锁,所有核心逻辑均在 ReentrantReadWriteLock 的内部类中实现。

二、源码解析

ReentrantReadWriteLock 的源码结构清晰,核心由三个部分组成:

  • ReadWriteLock 接口实现
  • 内部同步器 Sync(继承AQS)的实现。
  • 读锁(ReadLock)和写锁(WriteLock)的实现。

其中,Sync 是整个锁机制的核心,读写锁的获取、释放、重入等逻辑均依赖于 Sync 类,而 Sync 又分为公平模式(FairSync)和非公平模式(NonfairSync)两个子类。

整体类结构(简化)
public class ReentrantReadWriteLock implements ReadWriteLock, java.io.Serializable {
    // 读锁实例(内部类,实现Lock接口)
    private final ReentrantReadWriteLock.ReadLock readerLock;
    // 写锁实例(内部类,实现Lock接口)
    private final ReentrantReadWriteLock.WriteLock writerLock;
    // 核心同步器(继承AQS,实现锁的核心逻辑)
    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 ReentrantReadWriteLock.ReadLock readLock() { return readerLock; }
    // 获取写锁
    public ReentrantReadWriteLock.WriteLock writeLock() { return writerLock; }

    // 核心同步器(继承AbstractQueuedSynchronizer)
    abstract static class Sync extends AbstractQueuedSynchronizer { ... }

    // 非公平模式同步器
    static final class NonfairSync extends Sync { ... }

    // 公平模式同步器
    static final class FairSync extends Sync { ... }

    // 读锁实现(实现Lock接口)
    public static class ReadLock implements Lock { ... }

    // 写锁实现(实现Lock接口)
    public static class WriteLock implements Lock { ... }
}

核心:AQS状态的复用设计(关键难点)

AQS(AbstractQueuedSynchronizer)的核心是一个volatile修饰的int状态变量(state),用于表示锁的持有状态。对于ReentrantLock(排他锁),state=0表示无锁,state>0表示有线程持有锁(state的值即重入次数)。
ReentrantReadWriteLock需要维护读锁和写锁两种状态,而AQS只有一个state变量,因此它采用了状态复用的设计:将int类型的state(32位)拆分为两部分,高16位表示读锁的持有次数(共享锁计数),低16位表示写锁的持有次数(排他锁计数)。

abstract static class Sync extends AbstractQueuedSynchronizer {
    // 1. 写锁状态的掩码(低16位):用于提取写锁状态
    static final int EXCLUSIVE_MASK = (1 << 16) - 1; // 0x0000FFFF
    // 2. 读锁状态的移位位数(高16位):用于将读锁计数移到高16位
    static final int SHARED_SHIFT   = 16;
    // 3. 读锁每次重入,状态增加的值(1 << 16)
    static final int SHARED_UNIT    = (1 << SHARED_SHIFT); // 0x00010000
    // 4. 读锁最大持有次数(高16位最大为0xFFFF,即65535)
    static final int MAX_COUNT      = (1 << 16) - 1; // 0x0000FFFF
    // 5. 读锁最大重入次数(同MAX_COUNT)
    static final int MAX_SHARED_COUNT = MAX_COUNT;

    // 提取写锁状态(低16位)
    int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }
    // 提取读锁状态(高16位,右移16位)
    int sharedCount(int c)    { return c >>> SHARED_SHIFT; }
}

写锁(排他锁)源码解析:获取与释放

写锁是排他锁,实现类为ReentrantReadWriteLock.WriteLock,同样实现了Lock接口,核心方法委托给Sync同步器实现。写锁的获取逻辑核心是**“判断读锁或写锁是否被其他线程持有,若未被持有,则获取写锁;若被持有,则阻塞”**,同时支持重入、锁降级和公平/非公平模式。

写锁的获取(lock()方法)

WriteLocklock()方法本质是调用Syncacquire(1)方法,而acquire()AQS的排他锁获取模板方法,核心逻辑由SynctryAcquire()方法实现。

// WriteLock的lock()方法
public void lock() {
    sync.acquire(1);
}

// AQS的acquire()模板方法(排他锁获取)
public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt(); // 若获取失败,进入等待队列阻塞,支持中断
}

// Sync的tryAcquire()方法(核心:写锁获取逻辑)
protected final boolean tryAcquire(int acquires) {
    Thread current = Thread.currentThread();
    int c = getState();
    // 1. 写锁当前持有次数(低16位)
    int w = exclusiveCount(c);
    if (c != 0) { // 状态不为0,说明读锁或写锁被持有
        // ① 写锁持有次数为0(说明读锁被持有),或 ② 持有锁的线程不是当前线程 → 写锁获取失败
        if (w == 0 || current != getExclusiveOwnerThread())
            return false;
        // 2. 检查写锁重入次数是否超过最大值(65535)
        if (w + exclusiveCount(acquires) > MAX_COUNT)
            throw new Error("Maximum lock count exceeded");
        // 3. 写锁重入:CAS修改state(累加写锁计数)
        setState(c + acquires);
        return true;
    }
    // 4. 状态为0(无锁):尝试获取写锁(公平/非公平判断)
    if (writerShouldBlock() ||
        !compareAndSetState(c, c + acquires))
        return false;
    // 5. 获取写锁成功,设置当前线程为写锁持有者
    setExclusiveOwnerThread(current);
    return true;
}
写锁获取的核心逻辑拆解
  1. 锁状态判断若state != 0,说明读锁或写锁被持有:

    • w == 0(写锁未被持有),则说明读锁被持有(state高16位>0),此时写锁获取失败(读锁存在时,不能获取写锁,避免读脏数据)。

    • 若持有锁的线程不是当前线程,写锁获取失败(排他锁特性,只能有一个线程持有)。

  2. 重入校验:若当前线程持有写锁,则检查重入次数是否超过最大值(65535),若未超过,CAS修改state(累加写锁计数),重入成功。

  3. 无锁状态获取:若state == 0(无锁),则判断是否需要排队(writerShouldBlock()):

    • 非公平模式writerShouldBlock()返回false,直接CAS修改state,获取写锁。

    • 公平模式writerShouldBlock()返回true(判断等待队列是否有线程排队,若有则排队),此时不能插队。

  4. 锁持有者设置:获取写锁成功后,调用setExclusiveOwnerThread(current),将当前线程设为写锁持有者(AQS中的方法,用于标记排他锁的持有者)。

写锁的释放(unlock()方法)

写锁的释放逻辑核心是“递减写锁计数,若计数减为0,则释放写锁(清空锁持有者)”,释放后唤醒等待队列中的后续线程。

// WriteLock的unlock()方法
public void unlock() {
    sync.release(1);
}

// AQS的release()模板方法(排他锁释放)
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的tryRelease()方法(核心:写锁释放逻辑)
protected final boolean tryRelease(int releases) {
    // 1. 检查当前线程是否持有写锁(否则抛出异常)
    if (!isHeldExclusively())
        throw new IllegalMonitorStateException();
    // 2. 写锁计数递减(低16位)
    int nextc = getState() - releases;
    // 3. 判断写锁是否完全释放(计数减为0)
    boolean free = exclusiveCount(nextc) == 0;
    if (free) {
        setExclusiveOwnerThread(null); // 清空写锁持有者
    }
    // 4. 更新state(即使写锁未完全释放,也需更新状态,用于重入计数)
    setState(nextc);
    return free;
}

写锁释放的关键:只有当写锁的重入次数减为0时,才会清空锁持有者(setExclusiveOwnerThread(null)),并唤醒等待队列中的后续线程;若重入次数未减为0,则仅更新state,写锁仍被当前线程持有。

锁降级(写锁→读锁)

ReentrantReadWriteLock支持锁降级,即写线程持有写锁时,可获取读锁,再释放写锁,最终持有读锁。

锁降级的核心目的是“避免写锁释放后,其他线程修改数据,导致当前线程读取到脏数据”。

// 1. 获取写锁
writeLock.lock();
try {
    // 2. 执行业务逻辑(写操作)
    updateData();
    // 3. 获取读锁(写锁持有期间,可直接获取读锁,无需阻塞)
    readLock.lock();
} finally {
    // 4. 释放写锁(此时持有读锁,实现锁降级)
    writeLock.unlock();
}
try {
    // 5. 执行读操作(持有读锁,可并发读,且数据不会被修改)
    readData();
} finally {
    // 6. 释放读锁
    readLock.unlock();
}

注意:不支持读锁升级为写锁(读锁持有期间,无法获取写锁)。因为若多个线程同时持有读锁,均尝试升级为写锁,会导致死锁(每个线程都持有读锁,等待其他线程释放读锁,无法获取写锁)。

读锁(共享锁)源码解析:获取与释放

读锁是共享锁,实现类为ReentrantReadWriteLock.ReadLock,它实现了Lock接口,核心方法(lock()unlock())均委托给Sync同步器实现。
读锁的获取逻辑核心是“判断写锁是否被持有,若未被持有,则累加读锁计数;若被持有,则阻塞”,同时支持重入和公平/非公平模式。

读锁的获取(lock()方法)

ReadLocklock()方法本质是调用SyncacquireShared(1)方法,而acquireShared()是AQS的共享锁获取模板方法,核心逻辑由SynctryAcquireShared()方法实现(模板方法模式)。

// ReadLock的lock()方法
public void lock() {
    sync.acquireShared(1);
}

// AQS的acquireShared()模板方法(共享锁获取)
public final void acquireShared(int arg) {
    if (tryAcquireShared(arg) < 0)
        doAcquireShared(arg); // 若获取失败,进入等待队列阻塞
}

// Sync的tryAcquireShared()方法(核心:读锁获取逻辑)
protected final int tryAcquireShared(int unused) {
    // 获取当前线程
    Thread current = Thread.currentThread();
    // 获取当前AQS状态
    int c = getState();
    // 1. 判断写锁是否被持有(exclusiveCount(c) != 0),且持有写锁的线程不是当前线程
    // 若写锁被其他线程持有,读锁获取失败,返回-1(进入阻塞)
    if (exclusiveCount(c) != 0 &&
        getExclusiveOwnerThread() != current)
        return -1;
    // 2. 读锁计数(高16位)
    int r = sharedCount(c);
    // 3. 非公平模式下:判断是否需要阻塞(读者是否需要排队)、读锁计数是否超过最大值
    // 公平模式下,需先判断等待队列是否有线程排队(hasQueuedPredecessors())
    if (!readerShouldBlock() &&
        r < MAX_SHARED_COUNT &&
        compareAndSetState(c, c + SHARED_UNIT)) {
        // 4. 读锁获取成功:处理重入和首次获取的逻辑
        if (r == 0) { // 首次获取读锁(读锁计数从0变为1)
            firstReader = current;
            firstReaderHoldCount = 1;
        } else if (firstReader == current) { // 当前线程是首次获取读锁的线程(重入)
            firstReaderHoldCount++;
        } else { // 其他线程重入读锁,用ThreadLocal维护每个线程的读锁持有次数
            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; // 读锁获取成功,返回1
    }
    // 5. 若上述条件不满足(如读锁计数超界、需要排队),进入自旋重试获取读锁
    return fullTryAcquireShared(current);
}
读锁获取的核心逻辑拆解
  1. 写锁判断:首先判断写锁是否被持有(exclusiveCount( c ) != 0),若被持有,且持有写锁的不是当前线程,则读锁获取失败(返回-1),进入等待队列阻塞;若持有写锁的是当前线程,则允许获取读锁(支持写锁降级)。
  2. 读锁计数校验:读取当前读锁计数(r = sharedCount(c)),判断读锁计数是否超过最大值(65535),若超过则抛出异常(fullTryAcquireShared()中处理)。
  3. 公平/非公平判断
    • 非公平模式(默认):readerShouldBlock()返回false,无需排队,直接尝试CAS修改state(累加读锁计数)。
    • 公平模式:readerShouldBlock()返回true(判断等待队列是否有线程排队,若有则需要排队),此时不能插队,需进入等待队列。
  4. 重入处理:用两种方式维护读锁的重入次数:
    • 首次获取读锁的线程(firstReader):用变量firstReaderHoldCount记录重入次数(优化性能,避免频繁操作ThreadLocal)。
    • 其他线程:用ThreadLocal<HoldCounter>readHolds)维护每个线程的读锁重入次数,HoldCounter是一个内部类,存储线程ID和重入次数。
  5. 自旋重试:若CAS修改state失败(如并发修改冲突)、读锁计数超界等,进入fullTryAcquireShared()方法,通过自旋重试获取读锁,避免直接阻塞,提升性能。

读锁的释放(unlock()方法)

读锁的释放逻辑核心是“递减读锁计数,若计数减为0,则读锁完全释放”,同时维护重入次数的递减,释放后唤醒等待队列中的后续线程。

// ReadLock的unlock()方法
public void unlock() {
    sync.releaseShared(1);
}

// AQS的releaseShared()模板方法(共享锁释放)
public final boolean releaseShared(int arg) {
    if (tryReleaseShared(arg)) {
        doReleaseShared(); // 释放成功,唤醒等待队列中的后续线程
        return true;
    }
    return false;
}

// Sync的tryReleaseShared()方法(核心:读锁释放逻辑)
protected final boolean tryReleaseShared(int unused) {
    Thread current = Thread.currentThread();
    // 1. 判断当前线程是否是首次获取读锁的线程
    if (firstReader == current) {
        // 重入次数减1,若减为0,则清空firstReader
        if (firstReaderHoldCount == 1)
            firstReader = null;
        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(); // 重入次数为0,移除ThreadLocal中的记录
            if (count < 0)
                throw unmatchedUnlockException();
        }
        rh.count = count - 1;
    }
    // 3. CAS递减读锁计数(高16位)
    for (;;) {
        int c = getState();
        int nextc = c - SHARED_UNIT;
        if (compareAndSetState(c, nextc))
            // 读锁计数减为0时,返回true(唤醒后续线程);否则返回false
            return nextc == 0;
    }
}

读锁释放的关键:读锁是共享锁,多个线程持有读锁时,只有最后一个释放读锁的线程(读锁计数减为0),才会唤醒等待队列中的后续线程(可能是读线程或写线程),避免频繁唤醒,提升性能。

重入机制与公平/非公平模式补充

重入机制的底层实现

ReentrantReadWriteLock的重入机制,本质是通过“状态计数”和“线程持有标记”实现的:

  • 写锁重入:通过state的低16位(写锁计数)记录重入次数,当前线程持有写锁时,再次获取写锁,只需累加写锁计数(CAS修改state`)。

  • 读锁重入:通过firstReaderHoldCount(首次读线程)和ThreadLocal<HoldCounter>(其他线程)记录每个线程的读锁重入次数,再次获取读锁时,累加对应计数,同时累加state的高16位(读锁计数)。

公平与非公平模式的区别

公平与非公平模式的核心区别的是“线程获取锁时,是否允许插队”,由readerShouldBlock()writerShouldBlock()两个方法控制(Sync的抽象方法,FairSyncNonfairSync分别实现)。

// 非公平模式(NonfairSync)
static final class NonfairSync extends Sync {
    @Override
    final boolean writerShouldBlock() {
        return false; // 写锁无需排队,允许插队
    }
    @Override
    final boolean readerShouldBlock() {
        // 读锁仅在等待队列头部是写线程时,才需要排队(避免写线程饥饿)
        return apparentlyFirstQueuedIsExclusive();
    }
}

// 公平模式(FairSync)
static final class FairSync extends Sync {
    @Override
    final boolean writerShouldBlock() {
        // 写锁需要排队(判断等待队列是否有线程排队)
        return hasQueuedPredecessors();
    }
    @Override
    final boolean readerShouldBlock() {
        // 读锁需要排队(判断等待队列是否有线程排队)
        return hasQueuedPredecessors();
    }
}

补充说明:

  • 非公平模式(默认):性能更优,写锁和读锁均可插队获取锁,但可能导致线程饥饿(某些线程长期无法获取锁)。
  • 公平模式:线程按等待队列顺序获取锁,避免线程饥饿,但性能略差(频繁切换线程上下文)。

实战场景

适用场景

ReentrantReadWriteLock 最适合读多写少的高并发场景,例如:

  • 缓存系统:缓存查询(读)频繁,缓存更新(写)稀少。

  • 配置中心:配置读取(读)频繁,配置修改(写)稀少。

  • 日志系统:日志查询(读)频繁,日志写入(写)相对稀少。

注意:若写操作频繁(读少写多),不建议使用ReentrantReadWriteLock,因为写锁的排他性会导致读线程频繁阻塞,性能不如ReentrantLock(排他锁)。

常见坑点

  1. 读锁升级为写锁:严禁尝试读锁升级为写锁,会导致死锁。

  2. 锁释放顺序:重入场景下,读锁/写锁的释放次数必须与获取次数一致,否则会导致锁泄漏(锁无法完全释放)。

  3. 写锁饥饿:非公平模式下,若读操作过于频繁,写线程可能长期无法获取写锁(饥饿),可通过切换为公平模式解决,但需牺牲部分性能。

  4. 内存可见性:读锁和写锁均保证内存可见性(volatile statehappens-before关系),无需额外添加volatile修饰共享变量。

总结

ReentrantReadWriteLock 是Java并发编程中极具实用性的锁机制,其核心设计是“读写分离”和“状态复用”:通过拆分32位state变量,同时维护读锁(高16位)和写锁(低16位)的状态,实现了多线程并发读、单线程排他写的特性,同时支持重入、锁降级、公平/非公平模式,兼顾了并发安全性和性能。

核心源码逻辑回顾:

  • 读锁(共享锁):判断写锁是否被其他线程持有,若未被持有,累加读锁计数,支持重入;释放时递减计数,最后一个释放者唤醒后续线程。

  • 写锁(排他锁):判断读锁或写锁是否被其他线程持有,若未被持有,获取写锁,支持重入;释放时递减计数,计数为0时释放锁并唤醒后续线程。

  • 重入机制:通过state计数和ThreadLocal(读锁)、锁持有者标记(写锁)实现。

  • 公平/非公平模式:通过readerShouldBlock()writerShouldBlock()控制线程是否排队。

实际开发中,需根据场景选择是否使用ReentrantReadWriteLock——读多写少场景优先使用,可显著提升并发性能;读少写多场景建议使用ReentrantLock,避免写线程饥饿。掌握其底层原理,能帮助我们更合理地使用锁,写出高效、安全的并发程序。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值