ReentrantReadWriteLock
概述
它有两种可选择模式的锁,非公平锁和公平锁。
非公平锁的特质
受锁可重入性的约束,读锁和写锁获取的顺序是无法指定的,由于非公平锁被持续的竞争,所以可能无限期地延迟一个或者多 个读线程或者写线程,但通常具有比公平锁更高的吞吐量。
公平锁的特质
线程使用近似到达顺序策略获得锁。当当前持有的锁被释放时,要么为等待时间最长的单个写线程分配写锁,要么为等待时间比所有等待写线程都长的一组读线程分配读锁。
如果持有写锁,或者有一个写线程在等待,那么试图获得一个公平的读锁(非reentrantly)的线程将阻塞。在当前最老的等待写线程获得并释放写锁之后,线程才会获得读锁。当然,如果一个等待的写程序放弃了它的等待,在写锁是空闲的时候让一个或多个读程序线程作为队列中最长的等待程序,那么这些读程序将被分配读锁。
如果读锁和写锁都是空闲的(这意味着没有等待的线程),那么线程才能获得一个公平的写锁(非reentrantly),否则线程将会阻塞。
可重入性
这个锁允许读取器和写入器以ReentrantLock的样式重新获取读锁或写锁。在写线程持有的所有写锁都被释放之前,不允许使用非重入读取器。
此外,写入器可以获得读锁,但反之则不行。在其他应用程序中,当持有写锁时在调用或回调读锁下执行读的方法时,可重入性非常有用。如果一个读取器试图获得写锁,它将永远不会成功。
锁降级
可重入性还允许从写锁降级为读锁,降级的方法是先获取写锁,然后在获取读锁,然后释放写锁。然而,从读锁升级到写锁是不可能的。
中断获取锁
读锁和写锁都支持在锁获取期间中断。
类之间的关系
ReentrantReadWriteLock的内部调用比较复杂,把大致的流程的调用链顺序画成如下的样子,也是按照自己理解的样子画出来的,看起来只要自己能懂就好。
公平锁与非公平锁的ReaderLock、WriterLock调用链是一样的,下边逐步分析公平锁与非公平锁之间不同的地方即可。
lock和tryLock的区别
lock具有阻塞的性质,一旦获取不到,则进入AQS的等待对列中直到被unpark,而tryLock不会阻塞,如果获取不到锁的话,会立即返回false,否则返回true。
共享锁(读锁)获取过程源码解析
1.在获取读锁的时候,如果写锁被另外一个线程持有的话,那么此时获取读锁失败
2.如果此时写锁空闲或者被当前线程持有那么在通过readershouldblock去验证是否阻塞。
3.如果第二步失败的话,就执行fullTryAcquireShared.
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);// 获取读锁被共享的次数(可以同时被不同的线程共享)
// readerShouldBlock 在公平锁和不公平锁中逻辑是不一样的
// 公平锁逻辑是写锁空闲中,并且阻塞等待队列为空,或者当前线程是队列的头
// 不公平逻辑是当前阻塞队列中是否存在写锁的请求,存在的话则返回true
//
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;// cachedHoldCounter是用来存放最后一个获取对锁的线程,使用的目的是方便查询最后一个持有读锁线程的信息
if (rh == null || rh.tid != getThreadId(current))
cachedHoldCounter = rh = readHolds.get();// 使用get(),实际上是创建一个HoldCounter来更改最后一个获取读锁的线程的
else if (rh.count == 0)// rh.count==0说明当前线程获取过读锁
readHolds.set(rh);
rh.count++;
}
return 1;
}
return fullTryAcquireShared(current);// 这个方法是处理在上面CAS失败或者处理读锁重入失败的情况
}
tryAcquireShared不处理线程的读锁的重入,只要readerShouldBlock返回true,就不做处理,即使已经获取到果这个锁在这里也不处理。因为读锁只要当前线程获取过,是可以在次重入的。再次重入的过程放到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 we hold the exclusive lock; blocking here
// would cause deadlock.
} else if (readerShouldBlock()) { // 这里是处理在tryAcquireShared中没有处理读锁重入的情况,在full版本中来处理
// 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)// 这里比较重要,说明了可重入行,rh.count==0说明当前线程在阻塞之前没有持有过读锁,所以这里return -1是当前线程进入阻塞
return -1;
}
}
// 从这里开始处理在tryAcquireShared阶段CAS失败的情况
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;
}
}
}
源代码读到这里,才终于明白这两个字段是干什么的?为什么要加上它们?之前觉得不需要这些字段,也是很ok啊?
private transient ThreadLocalHoldCounter readHolds;
记录所有的获取read共享锁的线程和线程重入读锁的数量,如果count==0的话,说明当前线程没有获取过读锁
private transient HoldCounter cachedHoldCounter;
这个字段的设立,主要是用来缓存最新一个获取读锁的线程,因为在一般情况下,当前线程在查询自己的获取重入锁的情况时,先查一下缓存cached,如果没有查询到话,那么在到readHolds中查询,因为readHolds中保存了所有重入线程获取读锁的情况,使用readHolds查询的话,是比较耗时的。而且cachedHoldCounter中只保存了最近一个获取读锁的线程的信息,如果从cachedHoldCounter查询命中的话,肯定比在readHolds中查询耗时少,所以综上所述,这个字段的设立就是优化查询用的。
独占锁(写锁)获取过程源码解析
protected final boolean tryAcquire(int acquires) {
/*
* 概述:
* 1. If read count nonzero or write count nonzero
* and owner is a different thread, fail.
* 1.如果读锁被其他线程占用的话,失败
* 2. 如果写锁被重入的次数达到饱,那么会抛出错误异常
* 3. 申请写锁不合格或者CAS锁状态的话,则失败
* 4.以上均无失败的话,则修改独占锁占用的线程为当前线程
*/
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;
}
// writerShouldBlock的策略在公平锁和非公平锁中是不一样
if (writerShouldBlock() ||
!compareAndSetState(c, c + acquires))
return false;
setExclusiveOwnerThread(current);
return true;
}
在看看FairSync和NonfairSync之间有什么不同。
FairSync#readerShouldBlock()
final boolean readerShouldBlock() {
return hasQueuedPredecessors();
}
public final boolean hasQueuedPredecessors() {
// 如果AQS中的等待队列为空,或者当前线程是队列中的头结点,那么认为当前线程没有前驱节点
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());
}
FairSync#writerShouldBlock() 同readerShouldBlock()
NonfairSync#readerShouldBlock()
final boolean readerShouldBlock() {
*/
/* 避免写锁饿死,如果等待队列的头部存在等待写的线程的话,那么当前的读将会被阻塞
*/
return apparentlyFirstQueuedIsExclusive();
}
final boolean apparentlyFirstQueuedIsExclusive() {
Node h, s;
return (h = head) != null &&
(s = h.next) != null &&
!s.isShared() &&
s.thread != null;
}
NonfairSync#writerShouldBlock()
final boolean writerShouldBlock() {
return false; // writers can always barge
}
看到时直接返回false。也就是说只要是不公平的写锁的话,始终不阻塞。
共享锁(读锁)的释放
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))// 缓存查询失败,然后从readHolds中查询
rh = readHolds.get();
int count = rh.count;
if (count <= 1) {
readHolds.remove();// 因为当前线程释放后,count就会变成零,此时将从readHolds中将该线程的数据删除。
if (count <= 0) // 在当前还没有释放之前count已经是零了,那么说明当前线程已经释放完毕或者unlock此时和lock的次数匹配,所以这里抛出异常
throw unmatchedUnlockException();
}
--rh.count;// 当前线程持有的共享锁的重入次数减一
}
for (;;) {
int c = getState();
int nextc = c - SHARED_UNIT;
if (compareAndSetState(c, nextc))// 一次unlock 值释放一个SHARED_UNIT,如果CAS修改失败的话,则一直loop重试,直到成功返回
// 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;// 直到共享锁为零才代表是全部释放,返回true,否则返回false
}
}
独占锁(写锁)的释放
protected final boolean tryRelease(int releases) {
if (!isHeldExclusively())// 当前的线程在没有获取writelock的情况下,则抛出异常
throw new IllegalMonitorStateException();
int nextc = getState() - releases;
boolean free = exclusiveCount(nextc) == 0;// 获取独占锁的线程是可以重入的
if (free)
setExclusiveOwnerThread(null);
setState(nextc);
return free;
}
核心的方法已经分析完毕,最后总结一下知识点:
- 读锁是共享锁,可以被多个线程同时获取
- 写锁是独占锁,不能被不同的线程同时获取,但是可以被同一个线程同时重入获取独占锁
- 写锁被当前线程持有的时候,当前线程可以同时获取读锁,但是其他的线程的读锁申请将被阻塞
- 读锁被线程持有的时候,写锁申请将被阻塞
- 写锁被另外的线程持有时,那么当前线程的写锁申请将阻塞,但是那个已经持有写锁的线程,还可以获取写锁