ReentrantReadWriteLock

300 阅读8分钟

ReentrantReadWriteLock的核心思想是:允许多个读操作同时进行,读写操作互斥(即读时不能写,写时不能读);同时兼顾读写的性能。 个人非常建议先研究明白ReentrantLock,然后研究ReentrantReadWriteLock。因为相较而言ReentrantLock的逻辑更加简单,而且ReentrantReadWriteLock和ReentrantLock的很多逻辑(甚至是代码)都是相同的,熟悉ReentrantLock原理后学习ReentrantReadWriteLock将会容易很多。

1 核心属性

对于ReentrantReadWriteLock,控制读和写的锁是分开的;代码如下所示:

/** Inner class providing readlock */
private final ReentrantReadWriteLock.ReadLock readerLock;
/** Inner class providing writelock */
private final ReentrantReadWriteLock.WriteLock writerLock;
/** Performs all synchronization mechanics */
final Sync sync;

public ReentrantReadWriteLock.WriteLock writeLock() { return writerLock; }
public ReentrantReadWriteLock.ReadLock  readLock( { return readerLock; }

2 构造函数

以下是ReentrantReadWriteLock的构造函数,可以看到默认是非公平的。

public ReentrantReadWriteLock() {
    this(false);
}

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

3 获取读锁

获取读锁需满足以下条件:

  • 没有线程占用写锁(AQS的state变量低16位为0);或当前线程占有写锁(这其实是写锁降级为读锁)。即一个线程占有写锁时其他线程不能获取读锁。这一条件使得读锁与写锁互斥。
  • readerShouldBlock()方法返回false,即不阻塞获取读锁。
  • 当前累计读锁总数小于MAX_COUNT(2^16 -1,即65535)。因为state的低16位表示写锁状态、高16位表示读锁状态。
  • 成功通过CAS操作将读锁占有量+1(AQS的state高16位加1)。 获取读锁示例:reentrantReadWriteLock.readLock().lock()。

3.1 lock

public void lock() {
    sync.acquireShared(1);
}

注意:sync为ReentrantReadWriteLock# sync

3.2 acquireShared

/**
 * Acquires in shared mode, ignoring interrupts.  Implemented by
 * first invoking at least once {@link #tryAcquireShared},
 * returning on success.  Otherwise the thread is queued, possibly
 * repeatedly blocking and unblocking, invoking {@link
 * #tryAcquireShared} until success.
 *
 * @param arg the acquire argument.  This value is conveyed to
 *        {@link #tryAcquireShared} but is otherwise uninterpreted
 *        and can represent anything you like.
 */
public final void acquireShared(int arg) {
    if (tryAcquireShared(arg) < 0)
        doAcquireShared(arg);
}

3.3 tryAcquireShared

尝试获取锁,获取失败返回-1。此方法主要逻辑如下:

  • 如果已经有线程获取写锁,并且不是当前线程获取的写锁,则获取读锁失败。保证读写锁互斥。
  • 如果当前加锁次数达到MAX_COUNT,获取读锁失败。
  • 通过readerShouldBlock判断当前读线程是否应该阻塞;后面将详细介绍readerShouldBlock方法。
  • 可重入性是通过class ThreadLocalHoldCounter extends ThreadLocal threadlocal来保存重入获取锁的次数;
  • 然后调用fullTryAcquireShared方法对当前线程获取锁的次数进行操作。可以认为fullTryAcquireShared是tryAcquireShared的一个失败重试版本。代码详细解析见fullTryAcquireShared方法。
protected final int tryAcquireShared(int unused) {
    /*
     * Walkthrough:
     * 1. If write lock held by another thread, fail.
     * 2. Otherwise, this thread is eligible for
     *    lock wrt state, so ask if it should block
     *    because of queue policy. If not, try
     *    to grant by CASing state and updating count.
     *    Note that step does not check for reentrant
     *    acquires, which is postponed to full version
     *    to avoid having to check hold count in
     *    the more typical non-reentrant case.
     * 3. If step 2 fails either because thread
     *    apparently not eligible or CAS fails or count
     *    saturated, chain to version with full retry loop.
     */
    Thread current = Thread.currentThread();
    int c = getState();
    if (exclusiveCount(c) != 0 &&
        getExclusiveOwnerThread() != current)
        return -1;
    int r = sharedCount(c);
    // readerShouldBlock对于公平锁而言,如果当前队列中有节点在等待,那么返回false。对于非公平锁而言
    if (!readerShouldBlock() &&
        r < MAX_COUNT &&
        compareAndSetState(c, c + SHARED_UNIT)) {
        if (r == 0) {
            firstReader = current;
            firstReaderHoldCount = 1;
        } else if (firstReader == current) {
            firstReaderHoldCount++;
        } else {
            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;
    }
    return fullTryAcquireShared(current);
}

3.4 fullTryAcquireShared

/**
 * Full version of acquire for reads, that handles CAS misses
 * and reentrant reads not dealt with in tryAcquireShared.
 */
final int fullTryAcquireShared(Thread current) {
    /*
     * This code is in part redundant with that in
     * tryAcquireShared but is simpler overall by not
     * complicating tryAcquireShared with interactions between
     * retries and lazily reading hold counts.
     */
    HoldCounter rh = null;
    for (;;) {
        int c = getState();
        if (exclusiveCount(c) != 0) {
            // 处于写锁状态,如果不是当前线程占有写锁,那么返回-1;如果是当前线程占有写锁,那么可以尝试获取读锁操作。
            if (getExclusiveOwnerThread() != current)
                return -1;
            // else we hold the exclusive lock; blocking here
            // would cause deadlock.
        } else if (readerShouldBlock()) {
            // 写锁空闲,并且应该阻塞读锁
            // Make sure were not acquiring read lock reentrantly
            if (firstReader == current) {
                // 当前线程是第一个获取读锁的线程,什么都不做
                // assert firstReaderHoldCount > 0;
            } else {    // 当前线程不是第一个获取读锁的线程
                if (rh == null) {
                    rh = cachedHoldCounter;
                    if (rh == null || rh.tid != getThreadId(current)) {
                        rh = readHolds.get();
                        if (rh.count == 0)
                            readHolds.remove();
                    }
                }
                // 需要阻塞且是非重入(还未获取读锁的),获取失败。
                if (rh.count == 0)
                    return -1;
            }
        }
        if (sharedCount(c) == MAX_COUNT)
           // 读锁超过最大值了,抛出异常
            throw new Error("Maximum lock count exceeded");
        // 读锁+1
        if (compareAndSetState(c, c + SHARED_UNIT)) {
            if (sharedCount(c) == 0) {
                // 如果之前没有线程占据读锁,那么更新firstReader信息
                firstReader = current;
                firstReaderHoldCount = 1;
            } else if (firstReader == current) {
                // 有线程占据读锁,当前线程是firstReader时,count +1
                firstReaderHoldCount++;
            } else {
                // 有线程占据读锁,更新HoldCounter信息,即设定最后一次获取读锁的缓存
                if (rh == null)
                    rh = cachedHoldCounter;
                if (rh == null || rh.tid != getThreadId(current))
                    rh = readHolds.get();
                else if (rh.count == 0)
                    readHolds.set(rh);
                rh.count++;
                cachedHoldCounter = rh; // cache for release
            }
            return 1;
        }
    }
}

3.5 readerShouldBlock

对于公平锁而言,判断当前队列中是否有其他线程(在自己之前)等待获取锁;如果有,则应该阻塞。 对于非公平锁而言,判断当前队列中第一个等待节点是否申请的是写锁。如果是写锁,则应该阻塞。

3.5.1 公平锁

对于公平锁而言,如果队列不为空,并且当前线程前面有其他线程在等待,那么返回true。即如果有其他线程在当前线程之前等待锁,那么返回true,即应该阻塞。源码如下:

    final boolean readerShouldBlock() {
        return hasQueuedPredecessors();
    }

// 当且仅当队列中有等待线程并且当前线程不是第一个等待线程时返回true
public final boolean hasQueuedPredecessors() {
    Node t = tail; // Read fields in reverse initialization order
    Node h = head;
    Node s;
    return h != t &&
        ((s = h.next) == null || s.thread != Thread.currentThread());
}

3.5.2 非公平锁

对于非公平锁而言,其实是判断当前队列中第一个等待节点是否申请的是写锁。如果是写锁,那么应该阻塞读操作。 如何理解:可以设想一下,如果当前A线程获取了读锁,接着B线程过来申请写锁,然后来了很多线程申请读锁,因为读锁是共享的,所以可能导致写锁一致无法获取到而阻塞,这里就是为了解决这一个问题来,所以当已经有写锁在等待了,那么就让后续获取读锁的操作阻塞。

final boolean readerShouldBlock() {
    return apparentlyFirstQueuedIsExclusive();
}
//队列不为空,并且队列第一个等待节点是申请的是写锁,那么返回ture;其他情况返回false。
final boolean apparentlyFirstQueuedIsExclusive() {
Node h, s;
//队列不为空,并且队列第一个等待节点是申请的是写锁,那么返回ture
return (h = head) != null &&
    (s = h.next != null &&
    !s.isShared(        &&
    s.thread != null;
}

3.6 doAcquireShared

将当前线程加入等待队列中,等待获取读锁。主要逻辑是:

  • 创建一个SHARED类型的节点,加入到等待队列中。
  • 接下来无限循环,尝试进行以下操作,直到获取到锁或者因为取消获取锁而被唤醒。
  • 如果node是队列中第一个等待线程,那么尝试获取读锁,获取成功后更新队列的head,如果后一个节点也是等待读锁,那么后面一个节点的线程线程。这一过程实现读锁共享。
  • 判断是否应该挂起当前节点对应的线程,如果应该挂起,则通过LockSupport挂起线程。
  • 线程被唤醒以后,设置中断标志位。 代码如下:
/**
 * Acquires in shared uninterruptible mode.
 * @param arg the acquire argument
 */
private void doAcquireShared(int arg) {
    final Node node = addWaiter(Node.SHARED);
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {
            final Node p = node.predecessor();
            if (p == head) {
                // 如果节点的前一个节点是head,即自己处于等待队列的第一个位置,则尝试获取读锁。
                int r = tryAcquireShared(arg);
                if (r >= 0) {
                // 获取读锁成功,那么更新head,并且检查后续节点对应的线程是否处于等待读锁状态,如果时并且设置过waitStatus,则唤醒这些线程。
                    setHeadAndPropagate(node, r);
                    p.next = null; // help GC
                    if (interrupted)
                        selfInterrupt();
                    failed = false;
                    return;
                }
            }
                // 检查获取读锁失败以后是否应该挂起线程,如果应该挂起,那么通过LockSupport挂起线程
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

4 获取写锁

获取写锁满足以下条件:

  • 当前没有线程占用读锁,即(AQS state高16位为0);写锁未被占用(state低16位为0)或者占用写锁的线程是当前线程。这一条件保证读锁与写锁互斥,写锁重入性。
  • writerShouldBlock()方法返回false,即不阻塞写线程
  • 当前写锁重入次数小于最大值(2^16 -1),否则抛出Error
  • 通过CAS竞争将写锁状态+1(将state低16位+1) 获取读锁示例:reentrantReadWriteLock.writeLock().lock()。 获取写锁的过程和ReentrantLock中获取锁的过程基本上是一致的,所以下面将简略介绍。

4.1 lock

public void lock() {
    sync.acquire(1);
}

4.2 acquire

public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

以独占的方式获取锁,如果获取失败,那么进入等待队列中。

4.3 tryAcquire

略,详细可参考《ReentrantLock详解》

4.4 acquireQueued

略,详细可参考《ReentrantLock详解》

5 释放读锁

5.1 unlock

public void unlock() {
    sync.releaseShared(1);
}

5.2 releaseShared

public final boolean releaseShared(int arg) {
    if (tryReleaseShared(arg)) {
        doReleaseShared();
        return true;
    }
    return false;
}

释放锁,如果所有的读锁都释放了,那么执行doReleaseShared逻辑。

5.3 tryReleaseShared

主要完成两件事:

  • 设置线程读锁持有数量。
  • 释放线程持有的读锁,重试此操作直到成功。 当所有读锁均已释放,返回true。
protected final boolean tryReleaseShared(int unused) {
    Thread current = Thread.currentThread();
    if (firstReader == current) {
        // assert firstReaderHoldCount > 0;
        if (firstReaderHoldCount == 1)
            firstReader = null;
        else
            firstReaderHoldCount--;
    } else {
        HoldCounter rh = cachedHoldCounter;
        if (rh == null || rh.tid != getThreadId(current))
            rh = readHolds.get();
        int count = rh.count;
        if (count <= 1) {
            readHolds.remove();
            if (count <= 0)
                throw unmatchedUnlockException();
        }
        --rh.count;
    }
    for (;;) {
        int c = getState();
        int nextc = c - SHARED_UNIT;
        if (compareAndSetState(c, nextc))
            // Releasing the read lock has no effect on readers,
            // but it may allow waiting writers to proceed if
            // both read and write locks are now free.
            return nextc == 0;
    }
}

5.4 doReleaseShared

读锁释放完以后,通过此方法唤醒后续header后的节点或者设置header节点为PROPAGATE(传播),即:

  • 如果header节点waitStatus=SIGNAL,那么设置header节点的waitStatus=0,然后唤醒header后面的一个节点对应的线程。
  • 如果header的waitStatus=0,那么设置header节点的waitStatus= PROPAGATE
/**
 * Release action for shared mode -- signals successor and ensures
 * propagation. (Note: For exclusive mode, release just amounts
 * to calling unparkSuccessor of head if it needs signal.)
 */
private void doReleaseShared() {
    /*
     * Ensure that a release propagates, even if there are other
     * in-progress acquires/releases.  This proceeds in the usual
     * way of trying to unparkSuccessor of head if it needs
     * signal. But if it does not, status is set to PROPAGATE to
     * ensure that upon release, propagation continues.
     * Additionally, we must loop in case a new node is added
     * while we are doing this. Also, unlike other uses of
     * unparkSuccessor, we need to know if CAS to reset status
     * fails, if so rechecking.
     */
    for (;;) {
        Node h = head;
        if (h != null && h != tail) {
            int ws = h.waitStatus;
            if (ws == Node.SIGNAL) {
                if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                    continue;            // loop to recheck cases
                unparkSuccessor(h);
            }
            else if (ws == 0 &&
                     !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                continue;                // loop on failed CAS
        }
        if (h == head                  // loop if head changed
            break;
    }
}

6 释放写锁

释放过程和ReentrantLock中锁的释放过程基本相同,这里就不详细讲述了。