
ReentrantReadWriteLock的特点
普通的锁(如 ReentrantLock 或 synchronized)都是独占锁(排他锁),即无论读写,同一时刻只允许一个线程访问。但在很多实际场景中,读操作远多于写操作。如果读读也互斥,就会极大限制并发性能
ReentrantReadWriteLock 通过将锁拆分为读锁(ReadLock) 和 写锁(WriteLock),实现了“读读共享,读写互斥,写写互斥”,在读多写少的场景下能够大幅提升并发吞吐量

和ReentrantLock类似ReadWriteLock也分为公平锁和非公平锁
public ReentrantReadWriteLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
readerLock = new ReadLock(this);
writerLock = new WriteLock(this);
}
ReentrantReadWriteLock 维护了一对锁
读锁:允许多个线程同时获取,进行并发读取
写锁:同一时刻只允许一个线程获取,进行独占写入
| 锁状态 / 尝试获取 | 读锁 | 写锁 |
|---|---|---|
| 无锁 | 允许 | 允许 |
| 已加读锁 | 允许(读读共享) | 拒绝(读写互斥) |
| 已加写锁 | 拒绝(读写互斥) | 拒绝(写写互斥) |
从ReadWriteLock的行为我们可以猜到,写锁是互斥锁,读锁是共享锁,但是AQS中只提供了一个state变量来表示锁的状态。
我们如何用一个变量来存储两种锁的状态呢?
在ReadWriteLock中是这样做的,state变量的高16位表示读锁的状态,低16位表示写锁的状态

核心特性
可重入性
读锁和写锁都支持重入。同一个线程在持有锁的情况下,可以再次获取该锁而不会发生死锁(底层通过内部计数器记录重入次数)
锁降级(Lock Downgrading)与锁升级(Lock Upgrading)
支持锁降级:持有写锁的线程,可以先获取读锁,再释放写锁。这样该线程就从“写模式”平滑降级为“读模式”,且在这个过程中能保证数据一致性(防止其他线程在降级瞬间修改数据)
不支持锁升级:持有读锁的线程不能直接获取写锁。如果需要获取写锁,必须先释放读锁。若多个线程同时持有读锁并尝试升级为写锁,会导致死锁
公平与非公平策略
非公平模式(默认):吞吐量更高,但可能导致写线程“饥饿”(因为读线程源源不断占锁)
公平模式:按照 FIFO(先进先出)队列顺序获取锁。如果队列中有线程在等待写锁,后续尝试获取读锁的线程也会被阻塞,从而避免写线程饥饿
获取写锁
鉴于写锁的实现比较简单,我们就先看写锁的实现,再看读锁的实现
// WriteLock
public void lock() {
sync.acquire(1);
}
// AQS
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
上面的代码我们在AQS中已经分析过了,不再分析了,直接分析加锁的逻辑
// Sync
protected final boolean tryAcquire(int acquires) {
Thread current = Thread.currentThread();
int c = getState();
// 获取写锁的值
int w = exclusiveCount(c);
if (c != 0) {
// state不为0,写锁为0,说明读锁不为0
// (Note: if c != 0 and w == 0 then shared count != 0)
// 1. 读锁不为0
// 2. 写锁不为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;
}
从上面的源码中我们可以看到申请写锁的时候,只要有读锁就会失败,因此ReadWriteLock并不支持锁升级
加锁时公平锁和非公平锁的逻辑和ReentrantLock一样
static final class NonfairSync extends Sync {
// 非公平模式,直接cas去抢锁,抢不到再排队
final boolean writerShouldBlock() {
return false; // writers can always barge
}
}
static final class FairSync extends Sync {
// 同步队列中有线程则去排队
final boolean writerShouldBlock() {
return hasQueuedPredecessors();
}
}
释放写锁
// WriteLock
public void unlock() {
sync.release(1);
}
// AQS
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
直接看释放锁的逻辑
// Sync
protected final boolean tryRelease(int releases) {
// 解锁的线程和获取锁的线程不一样
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
int nextc = getState() - releases;
// 写锁是可重入的,判断所有的写锁是否都被释放
boolean free = exclusiveCount(nextc) == 0;
if (free)
setExclusiveOwnerThread(null);
setState(nextc);
return free;
}
将写锁的加锁次数减一,因为写锁是可重入的。当写锁都被释放时,唤醒同步队列中的线程,否则只是修改次数
获取读锁
// ReadLock
public void lock() {
sync.acquireShared(1);
}
// AQS
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
直接看加锁的逻辑
// Sync
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);
// 是否需要排队
// 是否超过能表示的加锁次数
// cas加锁
if (!readerShouldBlock() &&
r < MAX_COUNT &&
compareAndSetState(c, c + SHARED_UNIT)) {
if (r == 0) {
// 第一个获取读锁
firstReader = current;
firstReaderHoldCount = 1;
} else if (firstReader == current) {
// 读锁重入
firstReaderHoldCount++;
} else {
// cachedHoldCounter用来保存最后一个获取读锁的线程
HoldCounter rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current))
cachedHoldCounter = rh = readHolds.get();
else if (rh.count == 0)
readHolds.set(rh);
rh.count++;
}
// 从 AQS中acquireShared方法可以知道大于0表示获取到锁
return 1;
}
// 自旋获取读锁
return fullTryAcquireShared(current);
}
当我们加读锁的时候,如果有写锁并且不是当前线程就会加锁失败。如果有写锁并且是当前线程那么可以正常获取读锁,因此ReadWriteLock是支持锁降级的
firstReader,cachedHoldCounter等只是一些统计变量,例如读锁的获取次数,对主流程影响不大,不展开分析了
释放读锁
// ReadLock
public void unlock() {
sync.releaseShared(1);
}
// AQS
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
// Sync
protected final boolean tryReleaseShared(int unused) {
// 省略部分无关代码
for (;;) {
int c = getState();
// 将读锁次数减1
int nextc = c - SHARED_UNIT;
if (compareAndSetState(c, nextc))
// nextc == 0表示读锁和写锁都被释放了
return nextc == 0;
}
}
通过CAS不断减少读锁的加锁次数。
总结
读取是获取共享锁,在获取读锁之前会先判断写锁是否被获取,如果写锁被当前线程获取或者没有写锁,则获取读锁成功,否则获取读锁失败(支持锁降级)
写锁是获取独占锁,在获取之前会先判断读锁是否被获取,如果读锁已经被获取,则获取写锁失败。如果写锁没有被获取或者已经被当前线程获取,则获取写锁成功,否则获取写锁失败
本文详细解析了ReadWriteLock的特点,包括读写锁的工作原理、公平与非公平锁的区别,以及读写操作的互斥与并行性。重点介绍了写锁的获取与释放,以及读锁的获取策略,以及支持的锁升级与降级情况。
2632

被折叠的 条评论
为什么被折叠?



