本文将介绍Java中ReentrantReadWriteLock的实现原理,从JDK源码层面讲解读写锁的加锁、释放锁的流程,最后对流程进行总结。
读写锁概述
读写锁 ReentrantReadWriteLock 的依赖关系如下图所示。
读写锁的基本使用如下
ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
ReentrantReadWriteLock.ReadLock readLock = lock.readLock();
readLock.lock();
readLock.unlock();
ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock();
writeLock.lock();
writeLock.unlock();
读锁和写锁使用同一个 Sync 同步器,即使用同一个等待队列和 state。读锁状态使用 state 高 16 位存储,写锁状态使用 state 低 16 位存储。
读锁涉及两个重入计数:state(高 16 位)用于记录有多少个线程持有该读锁,HoldCounter 用于记录当前线程重入该读锁多少次,每个线程均有一个对应的 HoldCounter 对象。
写锁涉及一个重入计数:state(低 16 位)用于记录有多少个线程持有该写锁。
读锁不支持条件变量,写锁支持条件变量。
写锁加锁流程
WriteLock 的 lock() 方法会调用同步器的 acquire()方法。
// WriteLock implements Lock,
public void lock() {
sync.acquire(1);
}
同步器的 acquire() 方法会先调用 tryAcquire() 方法尝试获取写锁,获取写锁失败则调用 AbstractQueuedSynchronizer 的全参 acquire() 方法将线程加入等待队列。
// AbstractQueuedSynchronizer
public final void acquire(int arg) {
if (!tryAcquire(arg))
// 获取写锁失败则将线程加入等待队列
acquire(null, arg, false, false, false, 0L);
}
在介绍 ReentrantLock 的实现原理时,已对 AbstractQueuedSynchronizer 的全参 acquire() 方法进行了较为详细的介绍,可参照 源码解读 | Java中ReentrantLock的实现原理,此处不再赘述,重点来看一下 tryAcquire() 方法如何获取写锁。
// Sync extends AbstractQueuedSynchronizer
@ReservedStackAccess
protected final boolean tryAcquire(int acquires) {
Thread current = Thread.currentThread();
int c = getState();
// 写锁状态
int w = exclusiveCount(c);
// 已加写锁或者读锁
if (c != 0) {
if (
// 已加读锁:读写互斥,不能再加写锁
w == 0 ||
// 已加写锁,但不是自己加的(非写锁重入):写写互斥,不能再加写锁
current != getExclusiveOwnerThread()
)
// 加写锁失败
return false;
// 已加写锁且是自己加的
// 整数溢出
if (w + exclusiveCount(acquires) > MAX_COUNT)
throw new Error("Maximum lock count exceeded");
// 写锁重入
setState(c + acquires);
return true;
}
// 未加锁
if (
// 写锁应该等待
writerShouldBlock() ||
// 尝试获取写锁失败
!compareAndSetState(c, c + acquires)
)
// 加锁失败
return false;
// 加锁成功
// 将锁的持有者置为当前线程
setExclusiveOwnerThread(current);
return true;
}
NonfairSync 和 FairSync 对 writerShouldBlock() 有不同的实现。对于非公平锁,当前写线程可直接参与竞争,不应该等待,竞争失败才加入等待队列。对于公平锁,当前写线程不应该直接参与竞争,应该等待,加入等待队列。
// NonfairSync extends Sync
final boolean writerShouldBlock() {
// 直接参与竞争,不等待
return false;
}
// FairSync extends Sync
final boolean writerShouldBlock() {
// 等待队列中是否存在未取消的等待线程
// 存在未取消的线程则需要等待
return hasQueuedPredecessors();
}
读锁加锁流程
ReadLock 的 lock() 方法会调用同步器的 acquireShared() 方法。
// ReadLock implements Lock
public void lock() {
sync.acquireShared(1);
}
同步器的 acquireShared() 方法会先调用 tryAcquireShared() 方法尝试获取读锁,获取读锁失败则调用 AbstractQueuedSynchronizer 全参的 acquire() 方法将线程加入等待队列。
// AbstractQueuedSynchronizer
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
// 获取读锁失败则将线程加入等待队列
acquire(null, arg, true, false, false, 0L);
}
在介绍 ReentrantLock 的实现原理时,已对 AbstractQueuedSynchronizer 的全参 acquire() 方法进行了较为详细的介绍,可参照 [ReentrantLock 原理](),此处不再赘述,重点来看一下 tryAcquireShared() 方法如何获取写锁。
// Sync extends AbstractQueuedSynchronizer
@ReservedStackAccess
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-else分支用于读锁重入计数,忽略不影响理解主要加锁解锁流程
// 未加读锁
if (r == 0) {
// 第一个获取读锁的线程
firstReader = current;
firstReaderHoldCount = 1;
}
// 第一个获取读锁的线程重入
else if (firstReader == current) {
firstReaderHoldCount++;
}
// 已加读锁且当前线程非读重入
else {
// cachedHoldCounter缓存最近(上一个)获取读锁的线程的计数器
// rh:recent HoldCounter
HoldCounter rh = cachedHoldCounter;
if (
// 上一个获取读锁的线程为null
rh == null ||
// 上一个获取读锁的线程不是当前线程
rh.tid != LockSupport.getThreadId(current)
)
// 获取当前线程的计数器并缓存
cachedHoldCounter = rh = readHolds.get();
else if (
// 上一个获取读锁的线程为当前线程且计数为0(已被移除)
rh.count == 0
)
// 重新将当前线程放入哈希表记录
readHolds.set(rh);
// 更新计数(计数加1)
rh.count++;
}
return 1;
}
// 读线程应该等待或者尝试获取锁失败或者越界
// 再尝试获取读锁
return fullTryAcquireShared(current);
}
NonfairSync 和 FairSync 对 readerShouldBlock() 有不同的实现。对于非公平锁,只要等待队列第一个等待线程不是写线程,那么当前读线程就可直接参与竞争,不应该等待,竞争失败才加入等待队列。对于公平锁,当前读线程不应该直接参与竞争,应该等待,加入等待队列。
// NonfairSync extends Sync
final boolean readerShouldBlock() {
// 等待队列第一个等待线程是否为写线程
// 若为写线程,则读线程需要等待
// 不直接返回false是为避免写线程无限等待
return apparentlyFirstQueuedIsExclusive();
}
// FairSync extends Sync
final boolean readerShouldBlock() {
// 等待队列中是否存在未取消的等待线程
// 存在未取消的线程则需要等待
return hasQueuedPredecessors();
}
在 AbstractQueuedSynchronizer 的 全参acquire() 方法中,如果唤醒的是读线程,会调用 tryAcquireShared() 方法尝试获取锁,获取成功后,如果下一个等待线程是读线程,则会唤醒下一个等待线程。
// 如果读线程被唤醒,则调用 tryAcquireShared() 方法尝试获取锁
if (shared)
acquired = (tryAcquireShared(arg) >= 0);
// 如果读线程被唤醒并获取锁成功,并且下一个等待线程仍为读线程,则由当前读线程直接去唤醒下一个等待线程
if (acquired) {
...
if (shared)
signalNextIfShared(node);
...
}
// AbstractQueuedSynchronizer
private static void signalNextIfShared(Node h) {
Node s;
// 如果下一个等待线程为读线程,则唤醒下一个线程
if (h != null && (s = h.next) != null &&
(s instanceof SharedNode) && s.status != 0) {
s.getAndUnsetStatus(WAITING);
LockSupport.unpark(s.waiter);
}
}
写锁释放锁流程
WriteLock 的 unlock() 方法会调用同步器的 release()方法
// WriteLock
public void unlock() {
sync.release(1);
}
同步器的 release() 方法会先调用 tryRelease() 方法尝试释放写锁,释放成功后将唤醒下一个线程。
// AbstractQueuedSynchronizer
public final boolean release(int arg) {
if (tryRelease(arg)) {
signalNext(head);
return true;
}
return false;
}
公平锁和非公平锁释放写锁的流程相同。
// Sync extends AbstractQueuedSynchronizer
@ReservedStackAccess
protected final boolean tryRelease(int releases) {
// 写锁的持有者才能释放写锁
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
// 更新重入次数
int nextc = getState() - releases;
boolean free = exclusiveCount(nextc) == 0;
// 重入次数为0则将写锁的持有者置为null
if (free)
setExclusiveOwnerThread(null);
setState(nextc);
// 重入次数为0时认为写锁释放成功
return free;
}
读锁释放流程
ReadLock 的 unlock() 方法会调用 同步器的 releaseShared() 方法。
// ReadLock implements Lock
public void unlock() {
sync.releaseShared(1);
}
同步器的 releaseShared() 方法会先调用 tryReleaseShared() 方法尝试释放锁,释放成功后则会唤醒下一个等待线程。
// AbstractQueuedSynchronizer
public final boolean releaseShared(int arg) {
// 尝试释放读锁
if (tryReleaseShared(arg)) {
// 唤醒下一等待线程
signalNext(head);
return true;
}
return false;
}
公平锁和非公平锁释放读锁的流程相同。注意到,释放读锁时使用循环反复尝试,因为读锁是共享的,可被多个线程持有。
// Sync extends AbstractQueuedSynchronizer
protected final boolean tryReleaseShared(int unused) {
Thread current = Thread.currentThread();
// ...修改线程重入计数...
// 尝试释放锁
for (;;) {
int c = getState();
int nextc = c - SHARED_UNIT;
if (compareAndSetState(c, nextc))
// 重入次数为0时认为读锁释放成功
return nextc == 0;
}
}
读写锁流程总结
写锁加锁流程
获取写锁失败的情况
- 已加读锁,读写互斥;
- 已加写锁且非重入,写写互斥;
- 公平锁:等待队列存在未取消的线程。
获取写锁前必须释放读锁:
若线程 t 加读锁后,在释放读锁前,又加写锁,由于已加读锁,写锁获取失败,线程 t 陷入等待,又导致读锁无法释放,从而产生死锁。
读锁加锁流程
获取读锁失败的情况
- 已加写锁非重入,写读互斥;
- 非公平锁:等待队列中第一个等待线程为写线程;
- 公平锁:等待队列存在未取消的线程。
读线程被唤醒后,如果等待队列下一个等待线程为读线程,则会直接将其唤醒。
写锁释放流程
尝试释放写锁,释放成功后将下一个等待线程唤醒。
如果线程不是写锁的持有者而去尝试释放写锁,则会抛异常 IllegalMonitorStateException。
// Sync extends AbstractQueuedSynchronizer
// tryRelease()
//
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
仅当写锁重入计数为 0 时,才认为写锁释放成功。
读锁释放流程
尝试释放读锁,释放成功后将下一个等待线程唤醒。
如果线程不是读锁的持有者而去尝试释放读锁,则会抛异常 unmatchedUnlockException。
// Sync extends AbstractQueuedSynchronizer
// tryReleaseShared()
if (count <= 0)
// 重入计数小于等于0说明当前线程未持有读锁
throw unmatchedUnlockException();
仅当读锁重入计数为 0 时,才认为读锁释放成功。
END
如果觉得本文对您有一点点帮助,欢迎点赞、转发加关注,这会对我有非常大的帮助,如果有任何问题,欢迎在评论区留言或者后台私信,咱们下期见!
文章文档:公众号 字节幺零二四
回复关键字可获取本文文档。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。