头图

本文将介绍Java中ReentrantReadWriteLock的实现原理,从JDK源码层面讲解读写锁的加锁、释放锁的流程,最后对流程进行总结。

读写锁概述

读写锁 ReentrantReadWriteLock 的依赖关系如下图所示。

读写锁的基本使用如下

ReentrantReadWriteLock lock = new ReentrantReadWriteLock();

ReentrantReadWriteLock.ReadLock readLock = lock.readLock();
readLock.lock();
readLock.unlock();

ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock();
writeLock.lock();
writeLock.unlock();

读锁和写锁使用同一个 Sync 同步器,即使用同一个等待队列和 state。读锁状态使用 state 高 16 位存储,写锁状态使用 state 低 16 位存储。

读锁涉及两个重入计数:state(高 16 位)用于记录有多少个线程持有该读锁,HoldCounter 用于记录当前线程重入该读锁多少次,每个线程均有一个对应的 HoldCounter 对象。

写锁涉及一个重入计数:state(低 16 位)用于记录有多少个线程持有该写锁。

读锁不支持条件变量,写锁支持条件变量。

写锁加锁流程

WriteLock 的 lock() 方法会调用同步器的 acquire()方法。

// WriteLock implements Lock,
public void lock() {
    sync.acquire(1);
}

同步器的 acquire() 方法会先调用 tryAcquire() 方法尝试获取写锁,获取写锁失败则调用 AbstractQueuedSynchronizer 的全参 acquire() 方法将线程加入等待队列。

// AbstractQueuedSynchronizer
public final void acquire(int arg) {
    if (!tryAcquire(arg))
        // 获取写锁失败则将线程加入等待队列
        acquire(null, arg, false, false, false, 0L);
}

在介绍 ReentrantLock 的实现原理时,已对 AbstractQueuedSynchronizer 的全参 acquire() 方法进行了较为详细的介绍,可参照 源码解读 | Java中ReentrantLock的实现原理,此处不再赘述,重点来看一下 tryAcquire() 方法如何获取写锁。

// Sync extends AbstractQueuedSynchronizer
@ReservedStackAccess
protected final boolean tryAcquire(int acquires) {
    Thread current = Thread.currentThread();
    int c = getState();
    // 写锁状态
    int w = exclusiveCount(c);
    // 已加写锁或者读锁
    if (c != 0) {
        if (
            // 已加读锁:读写互斥,不能再加写锁
            w == 0 || 
            // 已加写锁,但不是自己加的(非写锁重入):写写互斥,不能再加写锁
            current != getExclusiveOwnerThread()
        )
            // 加写锁失败
            return false;
        // 已加写锁且是自己加的
        // 整数溢出
        if (w + exclusiveCount(acquires) > MAX_COUNT)
            throw new Error("Maximum lock count exceeded");
        // 写锁重入
        setState(c + acquires);
        return true;
    }
    // 未加锁
    if (
        // 写锁应该等待
        writerShouldBlock() ||
        // 尝试获取写锁失败
        !compareAndSetState(c, c + acquires)
    )
        // 加锁失败
        return false;
    // 加锁成功
    // 将锁的持有者置为当前线程
    setExclusiveOwnerThread(current);
    return true;
}

NonfairSync 和 FairSync 对 writerShouldBlock() 有不同的实现。对于非公平锁,当前写线程可直接参与竞争,不应该等待,竞争失败才加入等待队列。对于公平锁,当前写线程不应该直接参与竞争,应该等待,加入等待队列。

// NonfairSync extends Sync
final boolean writerShouldBlock() {
    // 直接参与竞争,不等待
    return false;
}
// FairSync extends Sync
final boolean writerShouldBlock() {
    // 等待队列中是否存在未取消的等待线程
    // 存在未取消的线程则需要等待
    return hasQueuedPredecessors();
}

读锁加锁流程

ReadLock 的 lock() 方法会调用同步器的 acquireShared() 方法。

// ReadLock implements Lock
public void lock() {
    sync.acquireShared(1);
}

同步器的 acquireShared() 方法会先调用 tryAcquireShared() 方法尝试获取读锁,获取读锁失败则调用 AbstractQueuedSynchronizer 全参的 acquire() 方法将线程加入等待队列。

// AbstractQueuedSynchronizer
public final void acquireShared(int arg) {
    if (tryAcquireShared(arg) < 0)
        // 获取读锁失败则将线程加入等待队列
        acquire(null, arg, true, false, false, 0L);
}

在介绍 ReentrantLock 的实现原理时,已对 AbstractQueuedSynchronizer 的全参 acquire() 方法进行了较为详细的介绍,可参照 [ReentrantLock 原理](),此处不再赘述,重点来看一下 tryAcquireShared() 方法如何获取写锁。

// Sync extends AbstractQueuedSynchronizer
@ReservedStackAccess
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);
    if (
        // 读锁不应该等待
        !readerShouldBlock() &&
        // 未越界
        r < MAX_COUNT &&
        // 尝试获取读锁成功
        compareAndSetState(c, c + SHARED_UNIT)
    ) {
        // 以下三个if-else分支用于读锁重入计数,忽略不影响理解主要加锁解锁流程
        // 未加读锁
        if (r == 0) {
            // 第一个获取读锁的线程
            firstReader = current;
            firstReaderHoldCount = 1;
        } 
        // 第一个获取读锁的线程重入
        else if (firstReader == current) {
            firstReaderHoldCount++;
        } 
        // 已加读锁且当前线程非读重入
        else {
            // cachedHoldCounter缓存最近(上一个)获取读锁的线程的计数器
            // rh:recent HoldCounter
            HoldCounter rh = cachedHoldCounter;
            if (
                // 上一个获取读锁的线程为null
                rh == null ||
                // 上一个获取读锁的线程不是当前线程
                rh.tid != LockSupport.getThreadId(current)
               )
                // 获取当前线程的计数器并缓存
                cachedHoldCounter = rh = readHolds.get();
            else if (
                // 上一个获取读锁的线程为当前线程且计数为0(已被移除)
                rh.count == 0
            )
                // 重新将当前线程放入哈希表记录
                readHolds.set(rh);
            // 更新计数(计数加1)
            rh.count++;
        }
        return 1;
    }
    // 读线程应该等待或者尝试获取锁失败或者越界
    // 再尝试获取读锁
    return fullTryAcquireShared(current);
}

NonfairSync 和 FairSync 对 readerShouldBlock() 有不同的实现。对于非公平锁,只要等待队列第一个等待线程不是写线程,那么当前读线程就可直接参与竞争,不应该等待,竞争失败才加入等待队列。对于公平锁,当前读线程不应该直接参与竞争,应该等待,加入等待队列。

// NonfairSync extends Sync
final boolean readerShouldBlock() {
    // 等待队列第一个等待线程是否为写线程
    // 若为写线程,则读线程需要等待
    // 不直接返回false是为避免写线程无限等待
    return apparentlyFirstQueuedIsExclusive();
}
// FairSync extends Sync
final boolean readerShouldBlock() {
    // 等待队列中是否存在未取消的等待线程
    // 存在未取消的线程则需要等待
    return hasQueuedPredecessors();
}

在 AbstractQueuedSynchronizer 的 全参acquire() 方法中,如果唤醒的是读线程,会调用 tryAcquireShared() 方法尝试获取锁,获取成功后,如果下一个等待线程是读线程,则会唤醒下一个等待线程。

// 如果读线程被唤醒,则调用 tryAcquireShared() 方法尝试获取锁
if (shared)
    acquired = (tryAcquireShared(arg) >= 0);
// 如果读线程被唤醒并获取锁成功,并且下一个等待线程仍为读线程,则由当前读线程直接去唤醒下一个等待线程
if (acquired) {
    ...
    if (shared)
        signalNextIfShared(node);
    ...
}
// AbstractQueuedSynchronizer
private static void signalNextIfShared(Node h) {
    Node s;
    // 如果下一个等待线程为读线程,则唤醒下一个线程
    if (h != null && (s = h.next) != null &&
        (s instanceof SharedNode) && s.status != 0) {
        s.getAndUnsetStatus(WAITING);
        LockSupport.unpark(s.waiter);
    }
}

写锁释放锁流程

WriteLock 的 unlock() 方法会调用同步器的 release()方法

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

同步器的 release() 方法会先调用 tryRelease() 方法尝试释放写锁,释放成功后将唤醒下一个线程。

// AbstractQueuedSynchronizer
public final boolean release(int arg) {
    if (tryRelease(arg)) {
        signalNext(head);
        return true;
    }
    return false;
}

公平锁和非公平锁释放写锁的流程相同。

// Sync extends AbstractQueuedSynchronizer
@ReservedStackAccess
protected final boolean tryRelease(int releases) {
    // 写锁的持有者才能释放写锁
    if (!isHeldExclusively())
        throw new IllegalMonitorStateException();
    // 更新重入次数
    int nextc = getState() - releases;
    boolean free = exclusiveCount(nextc) == 0;
    // 重入次数为0则将写锁的持有者置为null
    if (free)
        setExclusiveOwnerThread(null);
    setState(nextc);
    // 重入次数为0时认为写锁释放成功
    return free;
}

读锁释放流程

ReadLock 的 unlock() 方法会调用 同步器的 releaseShared() 方法。

// ReadLock implements Lock
public void unlock() {
    sync.releaseShared(1);
}

同步器的 releaseShared() 方法会先调用 tryReleaseShared() 方法尝试释放锁,释放成功后则会唤醒下一个等待线程。

// AbstractQueuedSynchronizer
public final boolean releaseShared(int arg) {
    // 尝试释放读锁
    if (tryReleaseShared(arg)) {
        // 唤醒下一等待线程
        signalNext(head);
        return true;
    }
    return false;
}

公平锁和非公平锁释放读锁的流程相同。注意到,释放读锁时使用循环反复尝试,因为读锁是共享的,可被多个线程持有。

// Sync extends AbstractQueuedSynchronizer
protected final boolean tryReleaseShared(int unused) {
    Thread current = Thread.currentThread();
    // ...修改线程重入计数...
    // 尝试释放锁
    for (;;) {
        int c = getState();
        int nextc = c - SHARED_UNIT;
        if (compareAndSetState(c, nextc))
            // 重入次数为0时认为读锁释放成功
            return nextc == 0;
    }
}

读写锁流程总结

写锁加锁流程

获取写锁失败的情况

  • 已加读锁,读写互斥;
  • 已加写锁且非重入,写写互斥;
  • 公平锁:等待队列存在未取消的线程。

获取写锁前必须释放读锁:

若线程 t 加读锁后,在释放读锁前,又加写锁,由于已加读锁,写锁获取失败,线程 t 陷入等待,又导致读锁无法释放,从而产生死锁。

读锁加锁流程

获取读锁失败的情况

  • 已加写锁非重入,写读互斥;
  • 非公平锁:等待队列中第一个等待线程为写线程;
  • 公平锁:等待队列存在未取消的线程。

读线程被唤醒后,如果等待队列下一个等待线程为读线程,则会直接将其唤醒。

写锁释放流程

尝试释放写锁,释放成功后将下一个等待线程唤醒。

如果线程不是写锁的持有者而去尝试释放写锁,则会抛异常 IllegalMonitorStateException。

// Sync extends AbstractQueuedSynchronizer 
// tryRelease()
//
if (!isHeldExclusively())
    throw new IllegalMonitorStateException();

仅当写锁重入计数为 0 时,才认为写锁释放成功。

读锁释放流程

尝试释放读锁,释放成功后将下一个等待线程唤醒。

如果线程不是读锁的持有者而去尝试释放读锁,则会抛异常 unmatchedUnlockException。

// Sync extends AbstractQueuedSynchronizer 
// tryReleaseShared()
if (count <= 0)
    // 重入计数小于等于0说明当前线程未持有读锁
    throw unmatchedUnlockException();

仅当读锁重入计数为 0 时,才认为读锁释放成功。

END

如果觉得本文对您有一点点帮助,欢迎点赞、转发加关注,这会对我有非常大的帮助,如果有任何问题,欢迎在评论区留言或者后台私信,咱们下期见!

文章文档:公众号 字节幺零二四 回复关键字可获取本文文档。


字节幺零二四
9 声望5 粉丝

talk is cheap, show me you code!