前言
ReentrantReadWriteLock锁是AQS的另一种实现,它做到了可重入、可中断,分为公平和非公平两类实现,并且实现了读锁和写锁两类同时控制。在使用时,读写锁持有的同一个Lock实例,通过控制锁的行为,及CLH节点状态,来操作读锁和写锁,对于写少读多的场景,能提高效率。
由于我对AQS及ReentrantLock的源码已经做过分析,因此本篇文章对经常出现的代码流程介绍的会较为粗略,初次接触ReentrantReadWriteLock源码的同学,建议先从前面的文章看起,这样反而更能清晰的理解。
类的定义
读写锁ReentrantReadWriteLock,实现接口ReadWriteLock,其内部定义了两个方法,分别获取读锁和写锁。
public interface ReadWriteLock {
Lock readLock();
Lock writeLock();
}
看一下ReentrantReadWriteLock内部相关实现:
public class ReentrantReadWriteLock
implements ReadWriteLock, java.io.Serializable {
private final ReentrantReadWriteLock.ReadLock readerLock;
private final ReentrantReadWriteLock.WriteLock writerLock;
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.WriteLock writeLock() { return writerLock; }
public ReentrantReadWriteLock.ReadLock readLock() { return readerLock; }
// ...
}
与ReadWriteLock接口相关的代码,包含两个锁的实例、一个同步器Sync、两个构造函数,其中无参构造函数默认实现了非公平同步器;初始化两个读写锁实例时,将this传入;接下来看一下读写锁类的实现。
实现Lock接口,内部持有一个Sync实例,由构造函数注入。
public static class ReadLock implements Lock, java.io.Serializable {
private final Sync sync;
protected ReadLock(ReentrantReadWriteLock lock) {
sync = lock.sync;
}
// ...
}
在翻看ReadLock的实例方法,都是实现的Lock接口,与ReentrantLock相似。而WriteLock的定义与ReadLock差不多,具体分析时再看。
直接进入重点AQS类的实现类Sync,在ReentrantReadWriteLock中,与ReentrantLock类似,分为公平同步器和非公平同步器,并且默认实现的是非公平同步器。
abstract static class Sync extends AbstractQueuedSynchronizer {...}
在Sync中定义了一组常量及方法,用于记录读锁和写锁数量,它们是:
static final int SHARED_SHIFT = 16; // 共享锁偏移量
static final int SHARED_UNIT = (1 << SHARED_SHIFT); // 1左移16位,共享锁的位置
static final int MAX_COUNT = (1 << SHARED_SHIFT) - 1; // 允许申请锁的最大数量
static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;// 得到低16位补码;这样的int值EXCLUSIVE_MASK的高16位都是0,低16位都是1
/** Returns the number of shared holds represented in count */
static int sharedCount(int c) { return c >>> SHARED_SHIFT; }
/** Returns the number of exclusive holds represented in count */
static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }
对于AQS的state值,存储的是一个32位int,在ReentrantReadWriteLock中,将高16位记录读锁数量,低16位记录写锁数量。
通过位移等运算,快速的得到需要的值,如sharedCount(int c),传入的c既是state值,通过无符号右移16位,快速得到读锁/共享锁的数量;
而exclusiveCount(int c)方法,传入state的值c,通过与EXCLUSIVE_MASK做&操作,能快速得到低16位的值是多少,也就是写锁/排它锁的数量。
c = 0110 0011 0000 1001 0000 0000 0000 0011
c & 0000 0000 0000 0000 1111 1111 1111 1111
0000 0000 0000 0000 0000 0000 0000 0011 // 因此有4个排它锁
在这之后,还定义了四个成员变量:
private transient ThreadLocalHoldCounter readHolds;
private transient HoldCounter cachedHoldCounter;
private transient Thread firstReader = null;
private transient int firstReaderHoldCount;
他们的作用在后面使用到的时候再介绍,这里看类的实现:
static final class HoldCounter {
int count = 0;
final long tid = getThreadId(Thread.currentThread());
}
static final class ThreadLocalHoldCounter extends ThreadLocal<HoldCounter> {
public HoldCounter initialValue() {
return new HoldCounter();
}
}
static final long getThreadId(Thread thread) {
return UNSAFE.getLongVolatile(thread, TID_OFFSET);
}
实现了ThreadLocal<?> ,记录了count和tid两个值,看样子是保持某些信息的。
在Sync中还有两个抽象方法
abstract boolean readerShouldBlock();
abstract boolean writerShouldBlock();
既然默认实现的是非公平同步器,那么我们来看一下NofairSync的实现,内容很简单:
static final class NonfairSync extends Sync {
final boolean writerShouldBlock() {
return false; // 永远返回false
}
final boolean readerShouldBlock() {
return apparentlyFirstQueuedIsExclusive(); // 调用一个AQS方法
}
}
final boolean apparentlyFirstQueuedIsExclusive() {
Node h, s;
return (h = head) != null &&
(s = h.next) != null &&
!s.isShared() &&
s.thread != null;
}
同步队列的首个节点不为空、队列不止一个节点、非head节点不是共享(存在排它锁等待)、非head节点的线程不为空。以上条件都满足,则认为当前读锁需要阻塞。
零散的源码介绍的差不多,到这应该已经一头雾水了,没关系,下面开始由读写锁的行为分析源码设计,并将上述内容串起来。
非公平同步器实现的写锁
定义了一个Sync类型对象sync,由构造函数传入,也就是这里定义了当前写锁是由“公平同步器实现”还是由“非公平同步器实现”实现的。
private final Sync sync;
protected WriteLock(ReentrantReadWriteLock lock) {
sync = lock.sync;
}
排它锁加锁流程分析
在WriteLock中,加锁方法直接使用的是sync.acquire(1); 追溯起来就是AQS的acquire()申请流程。
public void lock() {
sync.acquire(1);
}
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
那么tryAcquire()的实现,则是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) {
// (如果 c != 0 并且写锁数量为0,那么读锁数量肯定不为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;
}
方法内有大段的注释,翻译一下:
1、如果读锁数量不为0 或者 写锁数量不为零,并且 线程拥有者不是当前线程,获取失败
2、如果锁的数量已经达到最大值,获取失败
3、以上不满足,说明 可以申请锁,如果设置state成功,说明获取成功
这3条,囊括了tryAcquire()方法c!=0的大部分逻辑。
在代码中,if (w == 0 || current != getExclusiveOwnerThread()) 的潜在含义有2点:
1、当读写锁中存在读锁时,是不能直接申请到写锁的;
2、读锁是可以重入的
当c==0的情况下,也就是读写锁都为0,此时做了如下处理:
if (writerShouldBlock() ||
!compareAndSetState(c, c + acquires))
return false;
setExclusiveOwnerThread(current);
return true;
由于当前分析的是非公平同步器的实现,因此writerShouldBlock()==false。那么这段的逻辑就是 尝试直接修改state的值,如果设置成功,那么申请锁成功,设置排他线程拥有者、返回true。
在调用完tryAcquire(),后续流程就是AQS标准流程,这个在前文中已经详细描述过,这里不再赘述。
排它锁的tryLock
在WriteLock中实现的tryLock()方法,它的主逻辑是由Sync实现的。
public boolean tryLock( ) {
return sync.tryWriteLock();
}
final boolean tryWriteLock() {
Thread current = Thread.currentThread(); // 获取当前线程
int c = getState();// 获取资源
if (c != 0) { // 有锁
int w = exclusiveCount(c); // 计算排它锁数量
if (w == 0 || current != getExclusiveOwnerThread()) // 与tryAcquire逻辑一致
return false;
if (w == MAX_COUNT)
throw new Error("Maximum lock count exceeded");
}
if (!compareAndSetState(c, c + 1))
return false;
setExclusiveOwnerThread(current);
return true;
}
在tryWriteLock()方法中,相当于是单独执行了一次tryAcquire(),并且只是对state做+1操作。只是不需要考虑排它锁的阻塞情况,这也进一步说明“非公平”的特点。
排它锁带超时的tryLock
带有超时时间的tryLock,实现流程较为复杂,先看一下基本的调用关系:
public boolean tryLock(long timeout, TimeUnit unit)
throws InterruptedException {
return sync.tryAcquireNanos(1, unit.toNanos(timeout));
}
public final boolean tryAcquireNanos(int arg, long nanosTimeout) throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
return tryAcquire(arg) ||
doAcquireNanos(arg, nanosTimeout);
}
执行同步器的tryAcquireNanos()方法,具体的会执行tryAcquire()尝试直接申请一次锁,如果不成功会执行AQS的doAcquireNanos()。
private boolean doAcquireNanos(int arg, long nanosTimeout)
throws InterruptedException {
if (nanosTimeout <= 0L)
return false;
final long deadline = System.nanoTime() + nanosTimeout;
final Node node = addWaiter(Node.EXCLUSIVE);
boolean failed = true;
try {
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return true;
}
nanosTimeout = deadline - System.nanoTime();
if (nanosTimeout <= 0L)
return false;
if (shouldParkAfterFailedAcquire(p, node) &&
nanosTimeout > spinForTimeoutThreshold)
LockSupport.parkNanos(this, nanosTimeout);
if (Thread.interrupted())
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}
这个方法在分析AQS时做过详细分析,这里再写一下粗略的过程:
1、将当前线程封装为node节点,并加入同步队列,计算超时时间点
2、如果node的前置节点是head,则尝试申请锁tryAcquire(),否则进入3
3、当前时间如果未超时,并且剩余时间大于阈值,则将node的线程挂起,等待被唤醒或超时中断
4、被唤醒后重复申请直到申请成功或线程被中断
排它锁解锁流程分析
调用流程与加锁差不多,通过AQS的release实现,并且自定义了tryRelease():
public void unlock() {
sync.release(1);
}
protected final boolean tryRelease(int releases) {
if (!isHeldExclusively()) // 根据持有线程,拦截非法释放操作
throw new IllegalMonitorStateException();
// 后续代码,都是有效线程内操作
int nextc = getState() - releases; // 计算释放后资源的值
boolean free = exclusiveCount(nextc) == 0; // 计算释放后,排它锁的数量
if (free) // 如果排它锁数量为0,则清空持有的线程
setExclusiveOwnerThread(null);
setState(nextc); // 设置资源state的值
return free; // 返回排它锁是否还持有
}
// 当前线程是否持有排它锁
protected final boolean isHeldExclusively() {
return getExclusiveOwnerThread() == Thread.currentThread();
}
由此可知tryRelease()的逻辑很简单,只是操作state的值,以及释放后对 写锁值的检查。
到这里,WriteLock中主要成员已经分析完成。
非公平同步器实现的读锁
定义了一个Sync类型对象sync,由构造函数传入,也就是这里定义了当前写锁是由“公平同步器实现”还是由“非公平同步器实现”实现的。
private final Sync sync;
protected ReadLock(ReentrantReadWriteLock lock) {
sync = lock.sync;
}
共享锁加锁流程分析
在WriteLock中,加锁方法直接使用的是sync.acquire(1); 追溯起来就是AQS的acquireShared()申请流程。
public void lock() {
sync.acquireShared(1);
}
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
那么tryAcquireShared()的实现,则是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、如果存在写锁并被其他线程持有,获取失败
2、读锁是否阻塞、读锁数量是否超过最大值,能否直接修改state
3、如果修改state值成功,根据读锁数量、线程对象等信息,记录一些状态值
其中第二点在修改state时,采用的是compareAndSetState(c, c + SHARED_UNIT),其中c + SHARED_UNIT的含义是:
由于c是当前state的值,它包含读锁(高16位)和写锁(低16位)两部分,如果直接c+1得到的是写锁数量+1,而我们需要对读锁+1时,就要针对高16位进行操作,而SHARED_UNIT=1<<16,它就是读锁的+1标准值。
因此compareAndSetState(c, c + SHARED_UNIT) 的含义类似于compareAndSetState(r, r + 1).
当成个获取到读锁后,需要记录一些信息,在前文我们简单介绍过几个变量及方法,在这里都用到了。
static final class HoldCounter {
int count = 0; // 数量
final long tid = getThreadId(Thread.currentThread()); // 线程id
}
static final class ThreadLocalHoldCounter extends ThreadLocal<HoldCounter> {
public HoldCounter initialValue() {
return new HoldCounter();
}
}
Sync() {
readHolds = new ThreadLocalHoldCounter();
//...
}
private transient ThreadLocalHoldCounter readHolds; // 记录线程共享锁信息:HoldCounter
private transient HoldCounter cachedHoldCounter; // 当前操作读锁的计数器
private transient Thread firstReader = null; // 首个获取读锁的线程
private transient int firstReaderHoldCount; // 首个获取读锁的线程的 读锁计数器
static final long getThreadId(Thread thread) {
return UNSAFE.getLongVolatile(thread, TID_OFFSET); // 获取线程ID
}
回到tryAcquireShared()当中:
if (r == 0) { // 读锁在申请前数量为0
firstReader = current; // 记录当前线程
firstReaderHoldCount = 1; // 记录读锁申请数量
} else if (firstReader == current) { // 如果r!=0 并且第一个读锁申请者是当前线程
firstReaderHoldCount++; // 累加读锁数量
} else {
HoldCounter rh = cachedHoldCounter; // 获取状态缓存对象
if (rh == null || rh.tid != getThreadId(current)) // 如果线程id与缓存中不一致
cachedHoldCounter = rh = readHolds.get(); // 从ThreadLocal中获取当前线程的计数器对象
else if (rh.count == 0)
readHolds.set(rh); // 设置rh
rh.count++; // 累加读锁数量
}
也就是说,共享锁在持有过程中,同步器会记录:
1、持有共享锁的线程、持有的数量
2、首个持有共享锁的线程、持有的数量
在申请到共享锁后,会更新它们的值;猜测释放时,也会更新。
在tryAcquireShared()方法的底部,还有一个fullTryAcquireShared()的调用,如果当前同步队列的头结点不为空、队列中有等待的节点、等待节点申请的锁是排它锁时,那么共享锁的申请需要进行排队,而不是直接申请。而排队申请的处理,就在fullTryAcquireShared()方法中。
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) { // 存在排它锁
if (getExclusiveOwnerThread() != current) // 并不是当前线程持有排它锁
return -1; // 无法申请读锁
} else if (readerShouldBlock()) { // 读锁申请是否阻塞,也就是同步队列中是否存在写锁申请
// 如果申请读锁阻塞了,也就是有写锁在前面排队,那么就要考虑中断申请读锁了。
if (firstReader == current) {
} 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) // 如果当前线程读锁计数器为0,说明未申请到锁
return -1;
}
}
// 整理、统计 读锁计数器
if (sharedCount(c) == MAX_COUNT)
throw new Error("Maximum lock count exceeded");
// 这里的else if(xxx)判断,则是整个for(;;)自旋的基础
if (compareAndSetState(c, c + SHARED_UNIT)) { // 申请读锁资源
if (sharedCount(c) == 0) { //如果申请前读锁数量为0
firstReader = current;
firstReaderHoldCount = 1;
} else if (firstReader == current) { // 如果首个读锁申请者等于当前线程
firstReaderHoldCount++;
} else { // 当前线程并非首个申请读锁的线程,从ThreadLocal对象中获取计数器并更新
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;
}
}
}
当遇到readerShouldBlock时,ReadLock进入自旋方式,尝试申请锁。
共享锁的tryLock
在ReadLock中实现的tryLock()方法,它的主逻辑是由Sync实现的。
public boolean tryLock() {
return sync.tryReadLock();
}
final boolean tryReadLock() {
Thread current = Thread.currentThread();
for (;;) {
int c = getState();
if (exclusiveCount(c) != 0 &&
getExclusiveOwnerThread() != current)
return false;
int r = sharedCount(c);
if (r == MAX_COUNT)
throw new Error("Maximum lock count exceeded");
if (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 true;
}
}
}
在tryReadLock()方法中,相当于是单独执行了一次tryAcquireShared(),并且只是对state高16位做+1操作。只是不需要考虑共享锁的阻塞情况,也就没有自旋等待过程,这也进一步说明“非公平”的特点。
共享锁带超时的tryLock
带有超时时间的tryLock,实现流程较为复杂,先看一下基本的调用关系:
public boolean tryLock(long timeout, TimeUnit unit)
throws InterruptedException {
return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
}
public final boolean tryAcquireSharedNanos(int arg, long nanosTimeout) throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
return tryAcquireShared(arg) >= 0 ||
doAcquireSharedNanos(arg, nanosTimeout);
}
执行同步器的tryAcquireSharedNanos()方法,具体的会执行tryAcquireShared()尝试直接申请一次锁,如果不成功会执行AQS的doAcquireSharedNanos()。
private boolean doAcquireSharedNanos(int arg, long nanosTimeout)
throws InterruptedException {
if (nanosTimeout <= 0L)
return false;
final long deadline = System.nanoTime() + nanosTimeout;
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
for (;;) {
final Node p = node.predecessor();
if (p == head) {
int r = tryAcquireShared(arg);
if (r >= 0) {
setHeadAndPropagate(node, r);
p.next = null; // help GC
failed = false;
return true;
}
}
nanosTimeout = deadline - System.nanoTime();
if (nanosTimeout <= 0L)
return false;
if (shouldParkAfterFailedAcquire(p, node) &&
nanosTimeout > spinForTimeoutThreshold)
LockSupport.parkNanos(this, nanosTimeout);
if (Thread.interrupted())
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}
这个方法在分析AQS时做过详细分析,这里再写一下粗略的过程:
1、将当前线程封装为node节点,并加入同步队列,计算超时时间点
2、如果node的前置节点是head,则尝试申请锁tryAcquireShared(),设置头节点、唤醒后继节点申请共享锁;否则进入3
3、当前时间如果未超时,并且剩余时间大于阈值,则将node的线程挂起,等待被唤醒或超时中断
4、被唤醒后重复申请直到申请成功或线程被中断
共享锁解锁流程分析
ReadLock的解锁通过AQS的releaseShared()实现,并且自定义了tryRelease():
public void unlock() {
sync.releaseShared(1);
}
protected final boolean tryReleaseShared(int unused) {
Thread current = Thread.currentThread();
if (firstReader == current) { // 判断是否为firstReader,设置计数器
if (firstReaderHoldCount == 1)
firstReader = null;
else
firstReaderHoldCount--;
} else { // 非首次申请读锁的线程,从ThreadLocal中获取计数器并更新
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; // 高16位减一,得到新的读锁数量
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的值为0时,读锁完全释放(有读锁时不会存在写锁)
}
}
关注方法中的注释。值得注意的是,读锁的释放,会先修改计数器数值,然后自旋方式去释放state资源。
尾声
初看ReentrantReadWriteLock类,里面复杂的代码很容易放弃,需要学会对类进行拆解,其核心还是利用AQS来达到读写锁既要分离操作,又要互相影响的目的。