一、ReentrantReadWriteLock官方文档
https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/locks/ReentrantReadWriteLock.html
public class ReentrantReadWriteLock
implements ReadWriteLock, java.io.Serializable
1. 锁的获取顺序
对于获取获取锁的顺序,该类并没有特殊的偏向读者或写者。
不过,该类支持公平性和非公平性策略。
非公平策略
非公平策略也是默认的策略。在该策略下,读者和写者成功获取锁的顺序是不确定的。虽然在某些高竞争的情况下该策略可能会导致读者或写者饥饿,但是大多数情况下,该策略的吞吐量都很高。
公平策略
当使用公平策略时,线程得到锁的顺序基本上和到来的顺序(第一次尝试获取锁)一致。
当前被持有的锁释放后:
- 要么队列中等待最久的一个写者线程被唤醒(写线程在开头)
- 要么是队列中等待最久的一个或若干个读者线程被唤醒(读线程在开头)
当一个线程尝试获取公平读锁(非重入)时,如果遇到以下情况将会阻塞:
- 有线程持有写锁
- 没有线程持有写锁,但是有正在等待的写线程
当然,如果正在等待的写者放弃了等待,那么后续的读者就可以获取到读锁。
当一个线程尝试获取公平写锁(非重入)时,除非读锁和写锁都是空闲的(这说明等待队列中没有线程),否则会阻塞。
⚠️ 注意:
- 基于公平策略的读写锁的tryLock方法是不公平的!
- 上面为什么注明非重入,在下面分析源码的时候会看到!
2. 可重入性
-
ReentrantReadWriteLock允许同一个线程多次获取读锁,或者多次获取写锁。
-
当一个线程持有写锁时,除了该线程,其他线程不能获取读锁。
-
一个写者线程可以获取读锁,但反之则不行(读者线程不能获取写锁)。
-
一个读者线程尝试获取写锁永远不会成功。
3. 锁降级
ReentrantReadWriteLock支持锁降级:我们可以先加写锁,再加读锁,然后释放写锁。
然而,ReentrantReadWriteLock不支持锁升级。
4. 可中断的上锁
支持
5. 对Condition的支持
读锁不支持,写锁支持
二、ReentrantReadWriteLock源码
ReentrantReadWriteLock虽然叫Lock,但是它并没有实现Lock
接口,而是它所包含的readerLock和writerLock实现了Lock
接口:
public class ReentrantReadWriteLock
implements ReadWriteLock, java.io.Serializable
其中,ReadWriteLock接口仅仅定义了如下两个方法,分别用来返回readLock和writeLock:
public interface ReadWriteLock {
Lock readLock();
Lock writeLock();
}
ReentrantReadWriteLock包含的内部类如下:
1. ReadLock和WriteLock
其中,ReadLock和WriteLock是锁,实现了Lock接口;并且它俩是基于Sync的,它俩共享同一个Sync对象:
public class ReentrantReadWriteLock
implements ReadWriteLock, java.io.Serializable {
// ...
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 static class ReadLock implements Lock, java.io.Serializable {
private final Sync sync;
// 使用了ReentranReadWriteLock构造器传来的sync对象
protected ReadLock(ReentrantReadWriteLock lock) { sync = lock.sync;}
public static class WriteLock implements Lock, java.io.Serializable {
private final Sync sync;
// 同样使用了ReentranReadWriteLock构造器传来的sync对象
protected WriteLock(ReentrantReadWriteLock lock) { sync = lock.sync;}
}
下面列举出读写锁的Lock接口方法与Sync对象方法的对应关系(即读写锁的Lock接口方法底层调用了Sync对象的哪些方法):
WriteLock(独占) | Sync |
---|---|
lock() | acquire(1) --> tryAcquire(1) |
lockInterruptbily() | acquireInterruptibly(1) --> tryAcquire(1) |
tryLock() | tryWriteLock()【非AQS方法,固定非公平】 |
tryLock(time, unit) | tryAcquireNanos(1, unit.toNanos(timeout)) --> tryAcquire(1) |
unlock() | release(1) --> tryRelease(1) |
ReadLock(共享) | Sync |
---|---|
lock() | acquireShared(1) --> tryAcquireShared(1) |
lockInterruptbily() | acquireSharedInterruptibly(1) --> tryAcquireShared(1) |
tryLock() | tryReadLock() 【非AQS方法,固定非公平】 |
tryLock(time, unit) | tryAcquireSharedNanos(1, unit.toNanos(timeout)) --> tryAcquireShared(1) |
unlock() | releaseShared(1) --> tryReleaseShared(1) |
公平与非公平模式
读写锁分别都支持公平和非公平模式,分别对应FairSync和NonFairSync。但是上述表格中,无论是公平模式还是非公平模式调用的却都是父类Sync的方法,举个例子:fairSync.acquire(1)
和nonFairSync.acquire(1)
最终都会调用到父类的tryAcquire()
方法,因为这两个子类没有重写该方法,它们只重写了readerShouldBlock和writerShouldBlock抽象方法:
static final class NonfairSync extends Sync {
private static final long serialVersionUID = -8159625535654395037L;
final boolean writerShouldBlock() {
return false; // writers can always barge
}
final boolean readerShouldBlock() {
return apparentlyFirstQueuedIsExclusive();
}
}
static final class FairSync extends Sync {
private static final long serialVersionUID = -2274990926593161451L;
final boolean writerShouldBlock() {
return hasQueuedPredecessors();
}
final boolean readerShouldBlock() {
return hasQueuedPredecessors();
}
}
那么为什么调用同一个方法(tryAcquire
)就可以实现两种模式的呢?
就是借助上述两个方法,以写锁为例:
// Sync.tryAcquire
protected final boolean tryAcquire(int acquires) {
// ...
if (writerShouldBlock() || // ★实现公平性的要点就在这,writerShouldBlock是一个抽象方法,Sync类没有实现,交给子类
// FairSync和NonFairSync实现,子类的实现可以控制公平性!
!compareAndSetState(c, c + acquires))
return false;
setExclusiveOwnerThread(current);
return true;
}
关于公平性,下面还会再讲!
2. Sync
Sync类继承自AQS,是一个抽象类。
(1) state字段的划分
前面提到了,读锁和写锁共享同一个sync对象,⭐️那么它们自然就共享同一个同步状态state,也共享同一个同步队列。因此就需要对state进行划分,高16位专门用于记录读的状态,低16位专门用于记录写的状态:
关于这个状态的划分,Sync类里面定义了如下辅助字段和方法:
// SHARED表示读, EXCLUSIVE表示写
static final int SHARED_SHIFT = 16;
// 读锁计数用高位部分,读锁计数加1,其实是状态值加 2^16 (1 << SHARED_SHIFT)
static final int SHARED_UNIT = (1 << SHARED_SHIFT); // 0x00010000
// 最多支持的读锁/写锁计数: 65535个 (1 << SHARED_SHIFT) - 1
static final int MAX_COUNT = (1 << SHARED_SHIFT) - 1; // 0x0000ffff
// 即0x0000FFFF, 用于计算写锁的计数
static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1; // 0x0000ffff
/** 读锁计数 */
static int sharedCount(int c) {
return c >>> SHARED_SHIFT; // 右移16位
}
/** 写锁计数 */
static int exclusiveCount(int c) {
return c & EXCLUSIVE_MASK; // 取低16位
}
(2) 一些读操作相关的字段
注意:
- 下面的字段是所有访问同一个Sync对象的所有线程共用的!
- 下面的字段只和读操作有关!
// 记录每个线程的HoldCounter,因为readHolds是一个ThreadLocal变量,
// 所以不同的线程调用readHolds.get()会返回不同的值
private transient ThreadLocalHoldCounter readHolds;
// 记录上一个【成功获取读锁】的线程。这节省了查询ThreadLocal变量的开销。
private transient HoldCounter cachedHoldCounter;
// 记录第一个成功获取读锁的线程
private transient Thread firstReader = null;
// 记录第一个成功获取读锁的线程的重入次数
private transient int firstReaderHoldCount;
Sync() {
// ThreadLocal threadLocal = new ThreadLocal<>() {
// protected Object initialValue() { return new HoldCounter();}
// };
readHolds = new ThreadLocalHoldCounter();
setState(getState()); // ensures visibility of readHolds
}
static final class ThreadLocalHoldCounter
extends ThreadLocal<HoldCounter> {
public HoldCounter initialValue() { // 懒加载, 第一次get才调用initialValue
return new HoldCounter();
}
}
Q:为什么每个线程要维护一个HoldCounter?
A:答案很简单。对于一个支持重入的锁,我们必须有一种机制来记录重入次数,才能知道锁何时被完全释放,进而唤醒其他线程。
考虑独占锁的情况,例如ReentrantLock,由于同一时刻最多只有一个线程持有锁,所以同步状态state的值就可以表示线程重入次数。
而读锁就不一样了,多个线程可以同时持有读锁,修改state状态,所以单凭state状态是看不出哪个线程重入了多少次的,所以就需要额外的变量来记录每个线程的重入次数。每个线程都维护一个重入次数,显然用ThreadLocal变量最合适不过了,这就是HoldCounter存在的意义。
Q:firstReader,firstReaderHoldCount,cachedHoldCounter是干什么的?
A: 引入它们3个实际上是缓存,减少对readHolds的访问,也即减少了查找ThreadLocalMap的次数,提升了速度。
firstReader、firstReaderHoldCounter分别缓存第一个成功获取读锁的线程及其重入次数,这样,当第一个成功获取读锁的线程后面想要重入或者释放锁时,就可以直接修改firstReader和firstReaderHoldCounter缓存,而无需访问ThreadLocal!
从tryReleaseShared的源码中我们可以看到,当第一个成功获取读锁的线程完全释放锁状态之后,firstReader会被重置为null。
cachedHoldCounter也类似,它用缓存上一个成功获取读锁的线程的HoldCounter对象,HoldCounter对象里面存了线程的ID和重入次数。它缓存的至少是第二个成功获取读锁线程的HoldCounter,因为第一个线程的重入信息缓存在firstReader、firstReaderHoldCounter中了。
⚠️ 需要额外提一点的是,firstReader对应的线程,ReentrantReadWriteLock没有再把它的重入信息存到ThreadLocal变量readHolds中;而对于cachedHoldCounter对应的线程,ReentrantReadWriteLock会把它的重入信息(HoldCounter对象)设置到ThreadLocal变量readHolds中,并且cachedHoldCounter和readHolds.get()指向的是同一个HoldCounter对象!
这一点下面分析源码时也会看到。
(3) AQS相关方法
因为读锁和写锁都要用它,所以它既需要重写共享模式的方法,也需要重写独占模式的方法。
tryAcquire 独占锁(写锁)
注意该方法是针对写锁的
protected final boolean tryAcquire(int acquires) {
Thread current = Thread.currentThread();
// 1. 获取当前的写锁计数
int c = getState();
int w = exclusiveCount(c);
/// 2. 状态state不是0 (说明存在读线程或写线程)
// 大致思路: 判断是不是有线程在读? 没有的话是不是有线程在写? 写线程是不是自己?
if (c != 0) {
// 2.1 这条if语句成立的条件是:
// (1) c!=0且w==0, 说明存在线程持有读锁
// (2) c!=0且w!=0且current!=get..., 说明存在线程持有写锁,且持有写锁的线程不是自己
// 成立返回false表示获取锁失败
if (w == 0 || current != getExclusiveOwnerThread())
return false;
// 2.2 这条if语句成立的条件是:
// c!=0且w!=0且current==get..., 说明当前线程持有写锁,是重入
// 此时只要重入次数没超出限制(0xffff),即可成功获取锁
if (w + exclusiveCount(acquires) > MAX_COUNT)
throw new Error("Maximum lock count exceeded");
setState(c + acquires);
return true;
}
// 3. 状态state是0 (说明没有线程读,也没有线程写)
// 【writerShouldBlock】是Sync类的一个抽象方法,由子类FairSync和NonFairSync实现
// FairSync实现的是公平版本: {return hasQueuedPredecessors();} 只要同步队列中存在前驱就会导致下面返回false,表示获取锁失败,进而走阻塞的流程
// NonFairSync实现的是非公平版本: {return false;} 表示不应该阻塞
// 3.1 如果应该阻塞(存在前驱) 或 修改状态失败则返回false
if (writerShouldBlock() ||
!compareAndSetState(c, c + acquires))
return false;
// 3.2 获取独占锁成功 (因为之前状态是0,所以这里设置成功一定说明当前线程独占锁)
setExclusiveOwnerThread(current);
return true;
}
大致思路:就是判断各种状态state,分别采取不同的措施:
-
状态非0:说明有线程在读或写
-
只有读(w==0):获取写锁失败
-
只有写/有读有写:
- 如果不是当前线程在写,获取写锁失败;
- 如果是当前线程在写,说明是重入,并且因为写锁是独占的,直接用
setState
修改写状态即可(无需使用CAS)
注:有读有写一定是同一个线程又在读又在写
-
-
状态为0:说明没有线程读,也没有线程写,可以直接CAS加锁(同步状态+1),并将自己设置为ExclusiveOwnerThread。
为了实现公平性,CAS加锁之前需要调用
writerShouldBlock()
判断应不应该阻塞,FairSync和NonFairSync的实现分别如下:// fair // 如果队列中有正在等待的线程,则返回true,表示应该阻塞,导致获取锁失败,进入acquireQueued逻辑 final boolean writerShouldBlock() { return hasQueuedPredecessors();} // non fair // 直接返回false,表示不应该阻塞,可以直接进行CAS尝试加锁 final boolean writerShouldBlock() { return hasQueuedPredecessors();}
tryRelease 独占锁 (写锁)
注意该方法是针对写锁的
protected final boolean tryRelease(int releases) {
// 1. 释放锁必须先持有锁
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
// 2. 判断释放写锁后锁是不是空闲?
int nextc = getState() - releases;
boolean free = exclusiveCount(nextc) == 0;
// 如果是,修改ExclusiveOwnerThread
if (free)
setExclusiveOwnerThread(null);
// 3. 修改state
setState(nextc);
return free;
}
步骤如下:
- 先判断当前线程是不是持有锁
- 判断释放后写计数是不是0,如果是,说明写锁完全释放了,修改ExclusiveOwnerThread
- 调用setState修改同步状态-1(无需CAS,因为写锁独占)
tryAcquireShared 共享锁(读锁)
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.
* 否则,说明没有线程持有写锁,或者当前线程持有写锁,
* 则当前线程【可以对state状态上锁】,但是需要先询问
* 一下自己是不是需要阻塞:如果不需要阻塞,则尝试用CAS
* 更改读状态+1。
* 注意,该步骤中并没有处理【重入acquire】,【重入
* acquire】的处理被推迟到fullTryAcquireShared
* 中进行。这样做是有道理的,因为有很多情况都是【非重
* 入】,没必要检查重入计数。
* 3. If step 2 fails either because thread
* apparently not eligible or CAS fails or count
* saturated, chain to version with full retry loop.
* 如果步骤2因为【线程应该阻塞】或【CAS失败】或【读计数达到
* 上限】而失败了,则执行fullTryAcquireShared再次获取
*/
Thread current = Thread.currentThread();
int c = getState();
// 1. 存在其他线程在写,返回-1,失败
if (exclusiveCount(c) != 0 &&
getExclusiveOwnerThread() != current)
return -1;
// 2. 不存在其他线程在写(没有线程在写 或 当前线程在写)
int r = sharedCount(c);
// 如果当前读线程不应该被阻塞 且 读计数没到上限 且 CAS对state状态+1成功, 执行if语句体
if (!readerShouldBlock() &&
r < MAX_COUNT &&
compareAndSetState(c, c + SHARED_UNIT)) {
// ★ 注意: 走到这里说明已经CAS修改同步状态成功了! 下面【缓存一下获取锁的线程、读重入次数】
// 2.1 状态首次被读 (用firstReader, firstReaderHoldCount记录【当前线程】和【读重入次数1】)
if (r == 0) {
firstReader = current;
firstReaderHoldCount = 1;
}
// 2.2 同步状态不是首次被读,但是该线程之前读过且是第一个读的
// (这就说明【该线程】和【该线程的读重入次数】之前被记录过了,这里只需要修改一下【读重入次数++】即可)
else if (firstReader == current) {
firstReaderHoldCount++;
}
// 2.3 状态不是首次被读,且该线程不是第一个读的线程
// (这就说明firstXxx没有记录该线程,需要记录在cachedHoldCounter和readHolds中)
// (★ cachedHoldCounter: 记录上一个成功获取锁的【线程id】和【读重入次数】)
// (readHolds: 是一个ThreadLocal遍历,记录每个线程的【线程id】和【读重入次数】)
else {
HoldCounter rh = cachedHoldCounter;
// 2.3.1 如果【不存在上一个获得过锁的线程】 或者 【存在上一个成功获得过锁的线程但是不是当前线程】
// 则没法直接从【缓存】cachedHoldCounter中获取,需要到ThreadLocalMap中查找
if (rh == null
// rh == null说明没有上一个成功获取读锁的线程
// (也就是说当前线程是【第二个】成功获取读锁的线程。
// 为什么是第二个? 因为第一个成功获取读锁的线程记录在firstXxx中)
|| rh.tid != getThreadId(current))
// rh!=null && rh.tid != getThreadId(current)
// 说明【至少有两个】线程成功获取到读锁了,且上一个成功获取的线程
// 不是当前线程。则将cachedHoldCounter修改为自己的。
cachedHoldCounter = rh = readHolds.get();
// 2.3.2 之前至少有两个线程获取到了锁 且 上一个成功获取锁的线程 【是当前线程】
// 且 重入次数为0, 则【无需修改cachedHoldCounter(因为该线程上一次
// 成功获取后应该修改过了)】,只需要将缓存的rh设置到readHolds中
else if (rh.count == 0)
readHolds.set(rh);
// 2.3.3 重入次数++
rh.count++;
}
return 1;
}
// 3. 不存在其他线程写,但是当前线程应该阻塞 or 读计数达到上限 or CAS修改读状态失败(存在竞争)
return fullTryAcquireShared(current);
}
步骤如下:
- 先判断是不是有其他线程在写,如果是,直接返回-1获取失败
- 如果没有其他线程在写,阻塞*,读计数达到上限,CAS修改读状态+1是否成功,这3个条件只要一个不满足就跳到fullTryAcquireShared, 该方法的逻辑和tryAcquireShared差不多一样,只不过是再次尝试获取一次锁。
- 如果那3个条件都满足,说明已经CAS加读锁成功了,那么在if语句体内,需要缓存一下线程信息和重入次数,具体来说就是修改firstReader,firstReaderHoldCount,cachedHoldCounter。最后返回1表示成功获取锁。
- 如果上述3个条件存在不满足的,那么执行fullTryAcquireShared再次尝试获取
关于上述步骤2中的阻塞(readerShouldBlock()
),何时读者线程应该阻塞呢?
看一下该方法的实现就知道了:
// fair sync
// 还是和写的情况一样,只要同步队列中有等待的线程,且当前的读操作【不是重入】,就应该排队阻塞
final boolean readerShouldBlock() { return hasQueuedPredecessors(); }
// non fair sync
// 非公平机制就和写锁的时候不一样了,这时候会判断队列中的【第一个节点】是不是【独占模式(即写模式)】,
// 如果是,且当前的读操作【不是重入】,就应该排队阻塞
// 如果不这样做,而是直接返回false的话,那么即使队列中有等待的写线程,那么读操作也一直能抢先于这个写线程,导致写线程【饥饿】
final boolean readerShouldBlock() { return apparentlyFirstQueuedIsExclusive(); }
关于【不是重入】这一点很重要,如果是重入读(读->读,写->读)的话,是不需要考虑同步队列中是否有正在等待的线程的,可以直接获取锁,不需要阻塞。这一点在fullTryAcquireShared
源码中可以看到。
fullTryAcquireShared
/**
* Full version of acquire for reads, that handles CAS misses
* and reentrant reads not dealt with in tryAcquireShared.
* 再次尝试获取锁,处理tryAcquireShared中发生的【CAS失败】,并处理
* tryAcquireShared中【没处理的重入现象】
*/
final int fullTryAcquireShared(Thread current) {
HoldCounter rh = null;
for (; ; ) {
int c = getState();
// 1. 有线程在写
if (exclusiveCount(c) != 0) {
// 有线程在写,且不是自己
if (getExclusiveOwnerThread() != current)
return -1;
// else we hold the exclusive lock; blocking here
// would cause deadlock.
}
// 2. 如果读者应该阻塞(涉及重入情况的处理)
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;
}
}
// 走到这说明:
// (1) 当前线程在写
// (2) 没有线程在写,且读者应该阻塞,但当前线程是第一个读者
// (3) 没有线程在写,且读者不应该阻塞
// 3. 如果读者个数已经达到上限, 抛异常
if (sharedCount(c) == MAX_COUNT)
throw new Error("Maximum lock count exceeded");
// 4. 再次尝试CAS修改读状态+1
if (compareAndSetState(c, c + SHARED_UNIT)) {
// 修改成功
// 4.1 如果修改之前读状态为0,说明是第一次获取
if (sharedCount(c) == 0) {
firstReader = current;
firstReaderHoldCount = 1;
}
// 4.2
else if (firstReader == current) {
firstReaderHoldCount++;
}
// 4.3
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;
}
// CAS修改失败继续自旋
}
}
步骤如下:
(1) 首先,还是先看看是不是有其他线程在写,如果是,返回-1获取失败,否则下一步
(2) 回顾之前在tryAcquireShared中,如果线程应该阻塞,我们直接交给fullTryAcquireShared处理,此时fullTryAcquireShared就不能再“忽略”线程应该阻塞这个信息了,它需要作出处理,如果读线程应该阻塞的话(readerShouldBlock()
):
- 如果当前线程是第一个成功获取读锁的线程,就说明当前是重入读,那么可以直接获取读锁、不需要阻塞(代码中的步骤4)。虽然
readerShouldBlock()
返回true,表明了队列中有等待的线程(公平)或队列中有等待的线程且第一个是写线程(非公平),但是重入读就是那么厉害,它不需要阻塞。 - 否则,当前线程不是第一个成功获取读锁的线程,尝试从cachedHoldCounter或readHolds中获取其重入次数:
- 如果重入次数
rh.count==0
,说明当前线程是第一次获取锁,应该返回-1让它排队阻塞 - 否则,说明是重入,可以直接获取读锁、不需要阻塞(代码中的步骤4)
- 如果重入次数
其实,重入读不会被阻塞的原因也很明显,如果重入读允许被阻塞,那么就可能导致线程卡在重入读的lock步骤。
总结一下,正如fullTryAcquireShared方法的文档所说,它处理了tryAcquireShared没处理的一些重入情况,总结如下:
- 当前线程已经持有写锁,现在获取读锁,可直接获取(对应上述代码1处)
- 仅有线程持有读锁,且当前线程是其中一个,并且现在重入获取读锁(
rh.count!=0
),可以直接获取(对应上述代码2处)
(3) 查看读计数是不是达到了上限
(4) 尝试CAS修改同步状态,修改成功后缓存线程和重入次数信息
tryReleaseShared
protected final boolean tryReleaseShared(int unused) {
Thread current = Thread.currentThread();
// 1. 修改缓存中的线程信息和重入信息
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;
}
// 2. 不断循环尝试CAS修改同步状态
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;
}
}
该方法比较简单,就不再赘述。需要留意的是返回值:
- 如果返回true,表示修改后的同步状态state是0,也就是没有线程读也没有线程写
(4) 其他方法
tryWriteLock
final boolean tryWriteLock() {
Thread current = Thread.currentThread();
int c = getState();
if (c != 0) {
int w = exclusiveCount(c);
if (w == 0 || current != getExclusiveOwnerThread())
return false;
if (w == MAX_COUNT)
throw new Error("Maximum lock count exceeded");
}
if (!compareAndSetState(c, c + 1))
return false;
setExclusiveOwnerThread(current);
return true;
}
其实这里的逻辑和tryAcquire还是挺像的:
获取同步状态:
- 同步状态非0:说明有读或写 --> 只读 ?只写 ?有读有写?
- 同步状态为0:说明没有读没有写,CAS修改同步状态,设置setExclusiveOwnerThread
⭐️ 无论是公平模式还是非公平模式的写锁,都会调用该方法。但是从上面的分析也看到了,该方法中没有考虑队列中有没有等待的线程,而是直接尝试修改同步状态,所以是非公平的。所以说, 无论是公平模式还是非公平模式的写锁,其tryLock方法都是非公平的。
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;
}
}
}
同样,该方法的逻辑也和tryAcquireShared类似:
- 看看是不是有其他线程在写?
- 没有其他线程在写,判断是不是达到读计数上线了?
- 不是尝试CAS修改,修改成功缓存线程和重入计数信息
与tryReadLock一样,该方法也是非公平的!
三、读写线程的阻塞时机
经过上面的源码分析,相信已经对一个新来的读操作或者写操作会不会阻塞有个比较清晰的认识了,下面做一下总结。
当一个读或写操作到来时,此时锁的占有情况无非就一下几种:
- (无读无写)没有线程读也没有线程写
- (只读)只有线程只有读锁
- (只写)只有某一个线程只有写锁:
- (只写自己)持有写锁的线程是自己
- (只写其他)持有写锁的线程是其他线程
- (有读有写)说明只有一个线程持有写锁,且它重入获取了读锁,其实这种情况只不过是(只写)的一种特例,因此下文就没有特殊说明这种情况下的阻塞时机了,因为这种情况下的阻塞时机和(只写)情况一样。
而对于上述3类情况,根据锁是读锁还是写锁,是公平还是非公平,有可能有不同的表现,总结如下:
公平读锁的阻塞时机
- (无读无写):如果队列中仍然存在未被唤醒的等待线程,则阻塞;否则,不阻塞
- (只读):
- 如果是重入读,不会阻塞
- 如果是不是重入读,且同步队列中存在正在等待的写线程时,会排队阻塞
- (只写自己):是重入,不会阻塞(支持锁降级)
- (只写其他):会排队阻塞
非公平读锁的阻塞时机
- (无读无写):如果队列中第一个等待的线程是写线程,则阻塞;否则,不阻塞
- (只读):
- 如果是重入读,不会阻塞
- 如果是不是重入读,且同步队列中第一个等待的线程是写线程时,会排队阻塞
- (只写自己):是重入,不会阻塞 (支持锁降级)
- (只写其他):会排队阻塞
公平写锁的阻塞时机
- (无读无写):如果队列中仍然存在未被唤醒的等待线程,则阻塞;否则,不阻塞
- (只读):会排队阻塞
- (只写自己):是重入,不会阻塞
- (只写其他):会排队阻塞
非公平写锁的阻塞时机
- (无读无写):不管队列中有没有正在等待的线程,都不阻塞
- (只读):会排队阻塞
- (只写自己):是重入,不会阻塞
- (只写其他):会排队阻塞
四、锁降级
关于锁降级,第一节简单的介绍过了,ReentrantReadWriteLocks的文档给出了锁降级的一个例子:
class CachedData {
Object data;
volatile boolean cacheValid;
final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
void processCachedData() {
rwl.readLock().lock();
if (!cacheValid) {
// Must release read lock before acquiring write lock
rwl.readLock().unlock();
rwl.writeLock().lock();
try {
// Recheck state because another thread might have
// acquired write lock and changed state before we did.
if (!cacheValid) {
data = ...
cacheValid = true;
}
// Downgrade by acquiring read lock before releasing write lock
rwl.readLock().lock(); // ★锁降级
} finally {
rwl.writeLock().unlock(); // Unlock write, still hold read
}
}
try {
use(data);
} finally {
rwl.readLock().unlock();
}
}
}
所谓锁降级,就是在加写锁之后,释放写锁之前,对同一个读写锁的读锁进行加锁,如上面的代码所示。
这样做有什么意义呢?
这样做可以确保写锁释放后,其他线程对共享数据data的修改都会被阻塞,从而可以确保当前线程使用的data
数据是自己刚刚在写锁代码块中改过的,没有被其他线程修改。而如果将读锁的上锁写在写锁的解锁之后,那么该线程执行use(data)
时,很可能data
已经被其他线程修改过了。所以说,锁降级的应用之一就在于让一个线程可以读到自己刚刚写的数据。
那么,为什么ReentrantReadWriteLocks不支持锁升级(在读锁代码块中获取写锁)呢,其实从上述tryAcquire
的源码中可以看出,如果尝试进行锁升级操作的话,下面代码中c!=0
(因为当前线程正在读)且w==0
,导致该方法返回false,使得当前线程一直阻塞:
int c = getState();
int w = exclusiveCount(c);
if (c != 0) {
if (w == 0 || current != getExclusiveOwnerThread())
return false;
// ...
}