Java并发编程 - ReentrantReadWriteLock

ReentrantReadWriteLock使用示例

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class ReadWriteLockTest {

    private ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
    private Lock readLock = lock.readLock();
    private Lock writeLock = lock.writeLock();

    public void read() {
        try {
            readLock.lock();
            try {
                System.out.println("获得读锁 " + Thread.currentThread().getName() + " " + System.currentTimeMillis());
                Thread.sleep(10000);
            } finally {
                readLock.unlock();
            }
        } catch (InterruptedException ex) {
            ex.printStackTrace();
        }
    }

    public void write() {
        try {
            writeLock.lock();
            try {
                System.out.println("获得写锁 " + Thread.currentThread().getName() + " " + System.currentTimeMillis());
                Thread.sleep(10000);
            } finally {
                writeLock.unlock();
            }
        } catch (InterruptedException ex) {
            ex.printStackTrace();
        }
    }

    public static void main(String[] args) {
        ReadWriteLockTest readWriteLockTest = new ReadWriteLockTest();

        Thread a = new Thread(()->{
            readWriteLockTest.read();
        }, "t-A");

        Thread b = new Thread(()->{
            readWriteLockTest.write();
        }, "t-B");

        a.start();
        b.start();
    }

}

ReentrantReadWriteLock初始化

ReentrantReadWriteLock lock = new ReentrantReadWriteLock();

#ReentrantReadWriteLock构造方法

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);
}

实例化ReentrantReadWriteLock,其内部会自动创建ReadLock和WriteLock对象。

ReentrantReadWriteLock.ReadLock

protected ReadLock(ReentrantReadWriteLock lock) {
    sync = lock.sync;
}

ReentrantReadWriteLock.WriteLock

protected WriteLock(ReentrantReadWriteLock lock) {
    sync = lock.sync;
}

读锁和写锁使用的是同一个同步器。这就意味着state和同步队列被读锁和写锁共享。

#同步器静态变量

来看同步器中的几个静态变量:

ReentrantReadWriteLock.Sync

static final int SHARED_SHIFT   = 16;
static final int SHARED_UNIT    = (1 << SHARED_SHIFT);
static final int MAX_COUNT      = (1 << SHARED_SHIFT) - 1;
static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;

初始值如下:

  • SHARED_SHIFT = 16
  • SHARED_UNIT = 65536(1 0000 0000 0000 0000)
  • MAX_COUNT = 65535(1111 1111 1111 1111)
  • EXCLUSIVE_MASK = 65535(1111 1111 1111 1111)

#条件队列

读锁未实现条件队列,写锁实现了条件队列。

ReentrantReadWriteLock.ReadLock

/**
 * Throws {@code UnsupportedOperationException} because
 * {@code ReadLocks} do not support conditions.
 *
 * @throws UnsupportedOperationException always
 */
public Condition newCondition() {
    throw new UnsupportedOperationException();
}

ReentrantReadWriteLock.WriteLock

public Condition newCondition() {
    return sync.newCondition();
}

读锁

加锁流程分析

调用lock方法进行加锁:

ReadLock

 /**
 * Acquires the read lock.
 *
 * <p>Acquires the read lock if the write lock is not held by
 * another thread and returns immediately.
 *
 * <p>If the write lock is held by another thread then
 * the current thread becomes disabled for thread scheduling
 * purposes and lies dormant until the read lock has been acquired.
 */
public void lock() {
    sync.acquireShared(1);
}

这个方法的注释说了:如果有其他线程持有读锁,那么当前获取锁的这个线程就会等待直到获取到锁。

AbstractQueuedSynchronizer.java

public final void acquireShared(int arg) {
    if (tryAcquireShared(arg) < 0)
        doAcquireShared(arg);
}
@@通过tryAcquireShared获取令牌

ReentrantReadWriteLock.Sync

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);
    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);
}

1. 判断是否有其他线程持有写锁

如果有其他线程持有写锁,那么获取读锁的当前线程就要被挂起。

这个判断是下面这段逻辑代码:

if (exclusiveCount(c) != 0 && getExclusiveOwnerThread() != current)
    return -1;

看下exclusiveCount方法:

ReentrantReadWriteLock.Sync

static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }

EXCLUSIVE_MASK初始化时已经给出:

EXCLUSIVE_MASK = 65535(1111 1111 1111 1111)

假设只有读锁,怎么样保证exclusiveCount的结果为0呢?这就关系到读锁设置state的方式。

读锁修改state的方法如下:

compareAndSetState(c, c + SHARED_UNIT)

SHARED_UNIT初始化时已经给出:

SHARED_UNIT  = 65536(1 0000 0000 0000 0000)

现在我们假设有A、B、C三个线程,我们看一下执行这个设置后state的值是多少:

线程A执行完: 1 0000 0000 0000 0000
线程B执行完:10 0000 0000 0000 0000
线程C执行完:11 0000 0000 0000 0000

现在回到exclusiveCount方法:

static int exclusiveCount(int c) { return c & 65535(1111 1111 1111 1111); }

上面例子中可以看到state每次执行完后16位都是0,而65535二进制是16位1,&之后返回值为0。

读锁修改state规则:加锁加65536(1 0000 0000 0000 0000),解锁减65536(1 0000 0000 0000 0000)。

保证后16位为0

那么如果有写锁,怎么保证这里的返回不为0?其实也没那么复杂,只要保证写锁加锁对state设值之后后16位不为0就行了。

 setState(c + acquires);// acquires为1

写锁修改state规则:加锁加1,解锁减1。

读锁请求加锁只要观察到state的二进制数后16位不全为0,则说明有写锁操作了。

如果当前线程观察到有其他线程正持有写锁,则停止tryAcquireShared执行,当前线程会被加入到同步队列中。

2. 获取读锁被持有数量

上面我们说过读锁加锁每次state都会加65536(1 0000 0000 0000 0000),那么state如何反应读锁被持有的数量呢?通过下面的方法获取:

int r = sharedCount(c);

ReentrantReadWriteLock.Sync

static int sharedCount(int c)    { return c >>> SHARED_SHIFT;// SHARED_SHIFT = 16 }

还是拿上面那个例子:

线程A执行完: 1 0000 0000 0000 0000 // 无符号右移16位 = 0000 0000 0000 00001 = 1
线程B执行完:10 0000 0000 0000 0000// 无符号右移16位 = 00 0000 0000 0000 0010 = 2
线程C执行完:11 0000 0000 0000 0000// 无符号右移16位 = 00 0000 0000 0000 0011 = 3

3. 判断读是否已经阻塞

代码如下:

readerShouldBlock() 

线程能执行到这里,说明当前没有其他线程持有写锁。那么线程是否就可以继续执行呢?不一定,还要根据上面的方法来判断。

我们可以看到readerShouldBlock是定义在Sync中的抽象方法,具体逻辑由子类来实现。子类有FairSync和NonfairSync,公平和非公平模式,所以两种模式下会有不同的处理结果。

#公平模式

ReentrantReadWriterLock.FairSync

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

hasQueuedPredecessors在AbstractQueuedSynchronizer中定义:

AbstractQueuedSynchronizer

public final boolean hasQueuedPredecessors() {
    // The correctness of this depends on head being initialized
    // before tail and on head.next being accurate if the current
    // thread is first in queue.
    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());
}

公平模式说明:公平模式下,请求读锁的线程要检查同步队列,如果同步队列不为空,并且head节点的后继节点代表的线程不是当前线程,那么就阻塞当前线程。

这里逻辑跟我们ReentrantLock的公平模式一样,公平模式下新来的线程需要检查同步队列是否有比它等待获取锁时间更长的线程存在。

#非公平模式

ReentrantReadWriteLock.Nonfair

final boolean readerShouldBlock() {
    /* As a heuristic to avoid indefinite writer starvation,
     * block if the thread that momentarily appears to be head
     * of queue, if one exists, is a waiting writer.  This is
     * only a probabilistic effect since a new reader will not
     * block if there is a waiting writer behind other enabled
     * readers that have not yet drained from the queue.
     */
    return apparentlyFirstQueuedIsExclusive();
}

apparentlyFirstQueuedIsExclusive在AbstractQueuedSynchrinizer中定义:

AbstractQueuedSynchrinizer.java

/**
 * Returns {@code true} if the apparent first queued thread, if one
 * exists, is waiting in exclusive mode.  If this method returns
 * {@code true}, and the current thread is attempting to acquire in
 * shared mode (that is, this method is invoked from {@link
 * #tryAcquireShared}) then it is guaranteed that the current thread
 * is not the first queued thread.  Used only as a heuristic in
 * ReentrantReadWriteLock.
 */
final boolean apparentlyFirstQueuedIsExclusive() {
    Node h, s;
    return (h = head) != null &&
        (s = h.next)  != null &&
        !s.isShared()         &&
        s.thread != null;
}

在ReentrantLock非公平模式下新来的线程可以闯入式的获取锁而不用像公平模式那样去关心同步队列。

但是ReentrantReadWriteLock的非公平模式的机制有些不同,因为读写互斥,如果请求读锁的当前线程发现同步队列的head节点的下一个节点非互斥等待的节点,那么就说明有一个线程在等待获取写锁(争抢写锁失败,被放入到同步队列中),那么请求读锁的线程就要阻塞。

非公平模式说明:非公平模式下请求读锁的线程要检查是否有其他线程正尝试获取读锁(同步队列head的后继节点为互斥等待模式的节点)。

公平模式 vs 非公平模式:公平模式只要判断有比当前线程等待获取锁(读锁或写锁)时间更长的线程存在,则阻塞;非公平模式判断是否有等待获取写锁的线程存在,如果有,则阻塞。

4. 当前读锁总数判断

r < MAX_COUNT

MAX_COUNT = 65535,什么时候会达到最大值?请求读锁的线程获得读锁后不释放,直到请求次数达到最大值。

5. 通过CAS更新state值

compareAndSetState(c, c + SHARED_UNIT)

这个1中已经说明了。

6. 执行if块逻辑

3,4,5步条件都满足,则进入if语句块进行处理。

#6.1 处理第一次获取读锁的线程

if (r == 0) {
    firstReader = current;
    firstReaderHoldCount = 1;
}

ReentrantReadWriteLock.Sync

/**
 * firstReader is the first thread to have acquired the read lock.
 * firstReaderHoldCount is firstReader's hold count.
 *
 * <p>More precisely, firstReader is the unique thread that last
 * changed the shared count from 0 to 1, and has not released the
 * read lock since then; null if there is no such thread.
 *
 * <p>Cannot cause garbage retention unless the thread terminated
 * without relinquishing its read locks, since tryReleaseShared
 * sets it to null.
 *
 * <p>Accessed via a benign data race; relies on the memory
 * model's out-of-thin-air guarantees for references.
 *
 * <p>This allows tracking of read holds for uncontended read
 * locks to be very cheap.
 */
private transient Thread firstReader = null;
private transient int firstReaderHoldCount;

#6.2 处理第一次获取读锁的线程的重入

else if (firstReader == current) {
    firstReaderHoldCount++;
}

#6.3 处理非第一次获取读锁的线程

6.1、6.2条件不满足则继续执行:

 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++;
}

我们需要为每一个线程记录它持有读锁的次数,代码中通过ThreadLocal来解决这样的需求。

线程和它获取读锁次数的关联,通过HoldCounter包装起来:

AbstractQueuedSynchronizer.Sync

/**
 * A counter for per-thread read hold counts.
 * Maintained as a ThreadLocal; cached in cachedHoldCounter
 */
static final class HoldCounter {
    int count = 0;
    // Use id, not reference, to avoid garbage retention
    final long tid = getThreadId(Thread.currentThread());
}

HoldCounter对象通过ThreadLocalHoldCounter对象进行操作,ThreadLocalHoldCounter继承自ThreadLocal。

ReentrantReadWriteLock.Sync

static final class ThreadLocalHoldCounter extends ThreadLocal<HoldCounter> {
    public HoldCounter initialValue() {
        return new HoldCounter();
    }
}

这里涉及到ThreadLocal的使用,这里就不多讲,线程有关HoldCounter的内部数据如下:

Thread-HoldCounter.png

cachedHoldCounter做缓存使用。

6执行完后,返回1,表示线程获取读锁成功。

7. 执行fullTryAcquireShared

3,4,5步条件不能全满足,则执行fullTryAcquireShared。

可以看到上面我们设置state的时候使用了CAS,不过因为读锁是共享锁,所以可能会有其他线程也在执行CAS操作(获取写锁的线程也可能会与它抢着修改state),那么当前线程执行CAS就会失败,像其他共享模式的代码我们可以看到通常会自旋处理CAS的设置,而我们上面的代码却没有。所以为了处理这一情况,当CAS设置失败,那么就执行fullTryAcquireShared方法,fullTryAcquireShared会采用自旋的方式,fullTryAcquireShared的代码有很多与tryAcquireShared重复,这里就不做解释。

  • @@doAcquireShared

回到acquireShared方法:

AbstractQueuedSynchronizer.java

public final void acquireShared(int arg) {
    if (tryAcquireShared(arg) < 0)
        doAcquireShared(arg);
}

当线程获得读锁失败,那么就会执行doAcquireShared方法,执行这个会使得线程在同步队列中已共享模式等待。具体参考共享锁的分析。

参考:Java并发编程 - 共享锁

2. 解锁流程分析

ReentrantReadWriteLock.java

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

AbstractQueuedSynchronizer.java

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

ReentrantReadWriteLock.Sync

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;
    }
}

这个方法主要作用就是重设state的值并减计数值。

这里注意一下:nextc == 0,只有读锁全部释放之后,才会通知同步队列中等待的获取写锁而被阻塞的线程。

  • @@doReleaseShared
    与@@doAcquireShared一样,涉及到共享锁模式的实现原理,这里也不做说明。

参考:Java并发编程 - 共享锁

写锁

加锁流程分析

ReentrantReadWriteLock.java

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

AbstractQueuedSynchronizer.java

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

ReentrantReadWriteLock.Sync

protected final boolean tryAcquire(int acquires) {
    /*
     * Walkthrough:
     * 1. If read count nonzero or write count nonzero
     *    and owner is a different thread, fail.
     * 2. If count would saturate, fail. (This can only
     *    happen if count is already nonzero.)
     * 3. Otherwise, this thread is eligible for lock if
     *    it is either a reentrant acquire or
     *    queue policy allows it. If so, update state
     *    and set owner.
     */
    Thread current = Thread.currentThread();
    int c = getState();
    int w = exclusiveCount(c);
    if (c != 0) {
        // (Note: if c != 0 and w == 0 then shared count != 0)
        if (w == 0 || current != getExclusiveOwnerThread())
            return false;
        if (w + exclusiveCount(acquires) > MAX_COUNT)
            throw new Error("Maximum lock count exceeded");
        // Reentrant acquire
        setState(c + acquires);
        return true;
    }
    if (writerShouldBlock() ||
        !compareAndSetState(c, c + acquires))
        return false;
    setExclusiveOwnerThread(current);
    return true;
}

这段逻辑的描述的第一条:

If read count nonzero or write count nonzero and owner is a different thread, fail.

如果读的数量非零或者写的数量非零并且写锁持有者不是当前持有者那么就失败。

现在来看看上面说的控制是怎么实现的。

#第1种:读的数量非零,失败

int c = getState();
int w = exclusiveCount(c);

读的数量非零,则说明已经有线程持有了读锁,根据我们上面读锁获取锁的分析,state的值就是每次加上65536,比如:

线程A执行完: 1 0000 0000 0000 0000
线程B执行完:10 0000 0000 0000 0000
线程C执行完:11 0000 0000 0000 0000

如果state的值是这样的,这时候通过exclusiveCount获取写锁的数量,

static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }

得到的就是0。

下面有判断:

if (w == 0)
    return false;

w == 0, 失败。

读锁先被某线程请求获得到,并且未释放,其他线程就无法请求获得写锁。

#第2种:写的数量非零并且写锁持有者不是当前持有者那么就失败

写锁是互斥的。

当写锁已经被其他线程持有,当前线程进入此方法,会观察到c!=0&&w!=0,但是exclusiveOwnerThread不是当前线程,则无法获得读锁。

if (current != getExclusiveOwnerThread())
    return false;

看代码注释第二条:

If count would saturate, fail. (This can only happen if count is already nonzero.)

锁不被别同一个线程无限制地请求,而没有任何地释放。

if (w + exclusiveCount(acquires) > MAX_COUNT)
    throw new Error("Maximum lock count exceeded");

上面我们分析的是已经有线程对state修改的情况(读锁被抢到或写锁被抢到)。

现在来分析state=0的情况。

会有这种情况:同时有一个线程在获取读锁,另一个线程在获取写锁。但是两者都还没成功修改state,这时候state = 0,如何处理呢?

看下面代码:

if (writerShouldBlock() || !compareAndSetState(c, c + acquires))
    return false;

条件判断1:判断写是否应该被阻塞

和上面说的readerShouldBlock一样,也分为公平模式和非公平模式。

#非公平模式

ReentrantReadWriteLock.NonfairSync

final boolean writerShouldBlock() {
    return false; // writers can always barge
}

#公平模式

ReentrantReadWriteLock.FairSync

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

条件判断2:判断CAS设置是否成功设置

这个很好理解了,抢夺设置CAS失败,那么说明有其他线程在对state操作,不论它是获取读锁的线程还是获取写锁的线程。

  • @@acquireQueued

这个在ReentrantLock的讲解中已经分析过,这里不做分析。

解锁流程分析

ReentrantReadWriteLock.java

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

ReentrantReadWriteLock.Sync

protected final boolean tryRelease(int releases) {
    if (!isHeldExclusively())
        throw new IllegalMonitorStateException();
    int nextc = getState() - releases;
    
    if (free)
        setExclusiveOwnerThread(null);
    setState(nextc);
    return free;
}

这个方法比较简单,不做分析。

boolean free = exclusiveCount(nextc) == 0,写锁完全释放才会唤醒同步队列中的等待线程。

推荐阅读更多精彩内容