一、Hello World!
Java纪年1.5年,ReentrantReadWriteLock诞生于J·U·C。此后,国人一般称它为读写锁。人如其名,人如其名,她就是一个可重入锁,同时她还是一个读、写锁。
1.1 跟ReentrantLock并没有亲属关系
因为ReentrantReadWriteLock在命名上跟ReetrantLock非常贴近,很容易让人认为她跟ReentrantLock有继承关系,其实并没有。ReentrantReadWriteLock实现了ReadWriteLock和Serializable,同时ReadWriteLock跟Lock也没有继承关系。
ReentrantReadWriteLock跟ReentrantLock只有朋友关系,她们都是一个可重入锁。
但是ReentrantReadWriteLock的重入递归层级只有65535,即读锁能递归65535、写锁也同样能够递归65535层。至于为何是65535呢?在AQS框架的时候说过,AQS是用一个Integer来表示锁的状态。而一个Integer有32位,读用一半(16位)、写用一半(16位),16Bit就是65535。
1.2 对对对,我也有公平性
ReentrantReadWriteLock除与ReentrantLock一样是具有可重入性之外,她们具有公平性。既是她们都有公平锁和非公平锁的实现。实现方式也差不太远,关于公平性的内容可以翻阅JUC之JDK自带锁ReentrantLock。
二、About Me
ReentrantReadWriteLock提供一个读写分离的锁,读锁由ReadLock控制,写锁由WriteLock完成。当然读与写是互斥的。如你所知,可读不写,可写不读,即是读写不能同时进行,这就是读写锁,与众不同的自我。之所以能做到读写互斥说明她们最终还是用了同一个同步器(Sync),对的,她们Forwarding到上层(ReentrantReadWriteLock)的同步器。
我们可想而知,ReentrantReadWriteLock$Sync与ReentrantLock$Sync,她需要实现共享锁的功能。行了,按国际惯例还是先看一下源码吧。对不起,源码太长了,我们不看源码了。
// 此处没有源码
其实还是我认为她的源码简洁优雅易懂,大家完成能自行翻阅,大体思路如下:
- 当读锁被持有,不管是被一人持有,还是多人持有,写都需要阻塞。
- 当写锁被持有,当然只能有一个人持有了啦,此读锁将会被阻塞。
- 读写锁的阻塞方式直接由公平性决定,由FairSync或NonFairSync实现。
- 读锁可以有多人同时持有,因此需要记录持有者数量(HoldCounter)。
呵呵~,关键的代码后面涉及到了还是会拿出来看的。
2.1 读锁
读锁是一个共享锁,即是允许多个线程同时获得同一个锁。按惯例先看一下源码,我们用原来的方式单个单个流程看。
// ReentrantReadWriteLock$ReadLock 源码节选
public static class ReadLock implements Lock, java.io.Serializable {
private final Sync sync;
protected ReadLock(ReentrantReadWriteLock lock) {
sync = lock.sync;
}
public void lock() {
sync.acquireShared(1);
}
public boolean tryLock() {
return sync.tryReadLock();
}
public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException {
return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
}
public void unlock() {
sync.releaseShared(1);
}
}
这里可以看,ReadLock的功能最终还是由上级的ReentrantReadWriteLock实现的,这点很容易看出来。
这里的lock/tryLock/unlock
总体的流程与刚刚整理过的ReentrantLock基本一样,当然之前并没有实现过共享模式
,不过跟独占式差不多。
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
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 (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);
}
final int fullTryAcquireShared(Thread current) {
HoldCounter rh = null;
for (;;) {
int c = getState();
if (exclusiveCount(c) != 0) {
if (getExclusiveOwnerThread() != current)
return -1;
// else we hold the exclusive lock; blocking here
// would cause deadlock.
} else if (readerShouldBlock()) {
// Make sure we're 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");
if (compareAndSetState(c, c + SHARED_UNIT)) {
if (sharedCount(c) == 0) {
firstReader = current;
firstReaderHoldCount = 1;
} else if (firstReader == current) {
firstReaderHoldCount++;
} else {
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;
}
}
}
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) {
int r = tryAcquireShared(arg);
if (r >= 0) {
setHeadAndPropagate(node, r);
p.next = null; // help GC
if (interrupted)
selfInterrupt();
failed = false;
return;
}
}
if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
代码依然很长,这个让我很不开心。
先来看一下fullTryAcquireShared(Thread thread)
,代码很长,逻辑很简单。她返回值如果< 0
表示当前获取失败,> 0
表示获取成功。
- 当前独占锁已经被持有
自已持有了独占锁的话,此时应该是会死锁;如果不是自己持有返回-1
- 当前独占锁没有被持有
- 如果需要读阻塞
确定不是自己的重入,否则会发生死锁。如果当前没人持有返回-1
- 如果递归层级大于65535
报错 - 获得读锁
读锁持有者的递归层级+1
,返回1
- 获取读锁失败
如果读锁失败回到原点重走一回全流程
- 如果需要读阻塞
再往上看就是tryAcquireShared(int arg)
同样返回-1
和1
表示失败和成功。跟ReentrantLock的获取锁方式差不多。
- 第一个
if
判断的是是否独占锁,是否是自己持有。即判定为是否非重入,非重入则返回-1
- 第二个
if
判断的是是否有条件尝试获取锁,有条件则尝试,- 尝试成功则需要记录和统计读锁数量,和递归层级
- 尝试失败则进入上面
fullTryAcquireShared(Thread)
流程
如果上面tryAcquireShared(int arg)
没有成功获取到读锁的话,则将需要执行doAcquireShared(int arg)
继续尝试。
这里有个for(;;)
,但它并不会使CPU飙升,因为它的实际是有阻塞的,发生在parkAndCheckInterrupt()
由LockSupport.park(this);
完成。这里就是循环尝试和阻塞直到获取读锁成功。
2.1.1 公平
读锁的公平模式是在当前队列是否有独占锁被持有,若已被有则读锁需要阻塞;否则不需要阻塞,因为读锁是共享锁,只与写锁互斥。
这就很“公平”了,但其实这种模式的OPS比较低。
final boolean readerShouldBlock() {
return hasQueuedPredecessors();
}
2.1.2 非公平
人如其名,apparentlyFirstQueuedIsExclusive()
队头是不是独占锁。如果是独占的话会,读锁会一直阻塞。
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();
}
2.2 写锁
写锁依然是一个排他锁,即是只允许一个线程同时持有这把锁,也就是独占锁。按国际惯例先看代码吧。
public final void acquire(int arg) {
if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
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;
}
首先这里获取锁失败是指返回false
,写锁的获取流程跟ReentrantLock差不多。这里需要注意的是一旦执行了 setExclusiveOwnerThread(current)
,之后再想尝试读锁都会进入队列排队。
2.2.1 公平
写锁的公平模式跟读锁的公平模式差不多,非常符合我们的心理预期,所以很好理解。
final boolean writerShouldBlock() {
return hasQueuedPredecessors();
}
2.2.2 非公平
非公平模式在写锁上表现比较明显,写锁有总是能闯入。这个看起来可能有点超出心理预期,如果理解了会觉得更符合逻辑。
final boolean writerShouldBlock() {
return false; // writers can always barge
}
如果是在公平的获取策略下,其实写锁会永远被阻塞,当持有读锁请求的话。即是读多写少的情况下,此时是不是写一直拿不到锁呢?这是肯定的,所以此时非公平模式能有更高的OPS。
三、See You
在Java 1.8
之前它是JDK实现的读写锁的唯一实现,此后另有高人出山,高人是谁下回再说。
- ReentrantReadWriteLock是ReadWriteLock的唯一实现,在JDK。它由读、写锁组成,读是共享锁、写是独占锁,且读写互斥。
- ReentrantLock有很多类似的地方,比如都具有可重入性、都有两种获取锁的策略:公平与非公平,等。
- 与ReentrantLock一样在非公平模式能获得更高的OPS。