JUC 之 ReentrantReadWriteLock 读写锁
对于 ReentrantReadWriteLock 读写锁的实现,我们需要关注的是:如何处理 state 变量(如何通过一个单变量几表示写锁有表示读锁)以及读锁、写锁如何表示?下面我们就从这两方面入手开始学习。
核心成员介绍
private final ReentrantReadWriteLock.ReadLock readerLock; // 读锁实现
private final ReentrantReadWriteLock.WriteLock writerLock;// 写锁实现
final Sync sync; // 同步器实现,读写锁实现的核心均通过它来完成
public ReentrantReadWriteLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync(); // 区分公平与非公平
readerLock = new ReadLock(this);
writerLock = new WriteLock(this);
}
先来看看 ReadLock 和 WriteLock 的内部实现
ReadLock 实现
通过源码我们发现内部通过 Sync 来实现。读锁通过 AQS 的 acquireShared 和 releaseShared 两个核心方法实现共享锁机制。这两个方法已在【AQS 之 共享锁】中详细讲解过。
public static class ReadLock implements Lock, java.io.Serializable {
private final Sync sync;
// 获取锁,调用 sync 的 acquireShared 方法实现
public void lock() {
sync.acquireShared(1);
}
// 响应中断的方式获取锁,在等待过程中可被中断唤醒,调用 sync 的 acquireSharedInterruptibly 方法实现
public void lockInterruptibly() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
// 尝试获取读锁,获取不到立即返回,调用 sync 的 tryReadLock 方法实现
public boolean tryLock() {
return sync.tryReadLock();
}
// 带有等待超时的方式获取读锁,调用 sync 的 tryAcquireSharedNanos 方法
public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException {
return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
}
// 释放锁,调用 sync 的 releaseShared 方法
public void unlock() {
sync.releaseShared(1);
}
}
WriteLock 实现
基于 AQS 的 互斥锁原理实现写锁,详解请看【AQS 之 互斥锁】一篇。
public static class WriteLock implements Lock, java.io.Serializable {
private final Sync sync;
// 获取写锁,调用用 sync 的 acquire 方法
public void lock() {
sync.acquire(1);
}
// 以可响应中断的方式获取写锁,调用用 sync 的 acquireInterruptibly 方法
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
// 尝试获取写锁,获取失败立即返回,调用 sync 的 tryWriteLock 方法
public boolean tryLock( ) {
return sync.tryWriteLock();
}
// 尝试获取写锁,获取失败等待一段时间,调用 sync 的 tryAcquireNanos 方法,可被中断唤醒
public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException {
return sync.tryAcquireNanos(1, unit.toNanos(timeout));
}
// 释放写锁,调用 sync 的 unlock 方法
public void unlock() {
sync.release(1);
}
// 创建 Condition 条件对象
public Condition newCondition() {
return sync.newCondition();
}
}
通过上述源码我们发现读写锁实现的核心均在 Sync 中,下面我们详细讲解 Sync 这个内部类
Sync 实现
Sync 继承自 AbstractQueuedSynchronizer (AQS),拥有 AQS 的所有信息。它是所有同步器的核心基石。我们来看看 Sync 是如何利用AQS 所提供的信息完成读写锁功能的。学习下面内容时,大家要了解二进制的与运算、或运算、异或运算等。这里给大家一个口诀:
与运算:二进制截断,也即有零则为零;
或运算:二进制组合,也即有 1 则为 1;
异或运算:无进位加法,也即相同为 0,相异为 1;
1、将整型 state 变量切割为 高低 各16 位,高 16 位用于记录读锁信息,低 16 位用于记录写锁信息,通过二进制的位运算来获取读写锁信息
2、使用 HoldCounter 记录每个线程读锁的重入次数, 使用 ThreadLocal(ThreadLocalHoldCounter) 保存所有的 HoldCounter
核心成员介绍
abstract static class Sync extends AbstractQueuedSynchronizer {
static final int SHARED_SHIFT = 16;
static final int SHARED_UNIT = (1 << SHARED_SHIFT);// 用于对读锁加 1 操作
static final int MAX_COUNT = (1 << SHARED_SHIFT) - 1;// 写锁最大个数限制
static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;// 掩码,用于快速计算写锁个数
// 获取读锁个数,右移 16 位,取高 16 位的值
static int sharedCount(int c) { return c >>> SHARED_SHIFT; }
// 获取写锁个数,通过 与运算(&)将高 16 位截断,保留 低 16 位
static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }
// 该类用于保存线程信息以及该线程的锁重入次数
static final class HoldCounter {
int count = 0;
final long tid = getThreadId(Thread.currentThread());
}
private transient ThreadLocalHoldCounter readHolds;
private transient HoldCounter cachedHoldCounter;// 缓存最后一个获取读锁的线程
private transient Thread firstReader = null;// 记录第一个获取读锁的线程
private transient int firstReaderHoldCount;// 第一个读锁线程的重入次数
Sync() {
readHolds = new ThreadLocalHoldCounter();
setState(getState()); // ensures visibility of readHolds
}
}
核心方法介绍
tryAcquire 方法
该方法由 AQS 负责回调,实现具体的获取锁操作,也即如何操作 state 变量。写锁的实现逻辑同 互斥锁。两者唯一区别在于互斥锁操作的是整个 state 变量,而写锁操作的是 state 变量的低 16 位。
protected final boolean tryAcquire(int acquires) {
Thread current = Thread.currentThread();
int c = getState();// 保存当前 state 变量的值
int w = exclusiveCount(c);// 获取写锁个数
if (c != 0) {// 表示有现成持有锁,读或写
if (w == 0 || current != getExclusiveOwnerThread())// w = 0 表示有读线程持有锁,或者持有锁的线程不是当前线程,当前写线程阻塞
return false;
if (w + exclusiveCount(acquires) > MAX_COUNT)// 校验是否超过最大值
throw new Error("Maximum lock count exceeded");
setState(c + acquires);// 走到这一步说明持有锁的就是当前线程,也即锁重入
return true;
}
if (writerShouldBlock() || // 写锁是否需要阻塞,对于非公平锁来说永远返回false不要阻塞,而对于公平锁来说,需要看竞争队列中是否有等待节点
!compareAndSetState(c, c + acquires)) // CAS 抢锁
return false;
setExclusiveOwnerThread(current); // 走到这一步表明获取锁成功
return true;
}
tryWriteLock 方法
尝试获取写锁,不会阻塞线程,获取失败立即返回
final boolean tryWriteLock() {
Thread current = Thread.currentThread();
int c = getState(); // 保存当前 state 变量的值
if (c != 0) {
int w = exclusiveCount(c);// 获取写锁个数
// w = 0 表示有读线程持有锁,或者持有锁的线程不是当前线程,当前写线程阻塞
if (w == 0 || current != getExclusiveOwnerThread())
return false;
if (w == MAX_COUNT)
throw new Error("Maximum lock count exceeded");
}
if (!compareAndSetState(c, c + 1))// CAS 抢锁
return false;
setExclusiveOwnerThread(current);// 走到这一步表明获取锁成功
return true;
}
tryRelease 方法
释放写锁
protected final boolean tryRelease(int releases) {
if (!isHeldExclusively())// 校验当前线程是否是持有锁的线程,不允许释放其它线程的锁
throw new IllegalMonitorStateException();
int nextc = getState() - releases;// 写锁释放锁,只需要直接对 state 进行减法操作即可
boolean free = exclusiveCount(nextc) == 0; // 等于 0 说明是最后一个写锁释放锁
if (free)
setExclusiveOwnerThread(null);// 清空 exclusiveOwnerThread 信息
setState(nextc);// 更新 state
return free;
}
tryAcquireShared 方法
获取读锁,由于读锁是共享的,也即可以多个线程同时持有,为了记录读锁的重入次数这里引入了 HoldCounter 和 ThreadLocal,每个线程一个 HoldCounter 对象,用于记录该线程的所重入次数,然后将其保存在 ThreadLocal 中。如果是第一个读锁线程,这里通过 firstReader 和 firstReaderHoldCount 两个变量来进行优化,通过它俩保存线程信息和重入次数。
protected final int tryAcquireShared(int unused) {
Thread current = Thread.currentThread();
int c = getState();// 保存当前 state 变量的值
if (exclusiveCount(c) != 0 &&// 有写锁在等待,并且是其它线程正持有写锁
getExclusiveOwnerThread() != current)
return -1;
int r = sharedCount(c);// 保存当前读锁个数
// 读线程是否需要阻塞,竞争队列中有等待节点阻塞(公平锁),等待在队列首部的是互斥线程阻塞(非公平锁)
if (!readerShouldBlock() &&
r < MAX_COUNT &&
compareAndSetState(c, c + SHARED_UNIT)) {// CAS 获取读锁,并对读锁个数加 1
if (r == 0) {// 如果是第一个获取读锁的线程,使用 firstReader 进行优化处理
firstReader = current;
firstReaderHoldCount = 1;
} else if (firstReader == current) {// 所重入
firstReaderHoldCount++;
} else {// 非第一个获取读锁的线程 使用 HoldCounter 记录线程信息以及所重入次数
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;
}
// 走到此步说明读锁需要阻塞或 CAS 抢锁失败,fullTryAcquireShared 是完整版的抢锁流程,上一步是对程序的一种优化行为,判断前置优化
return fullTryAcquireShared(current);
}
fullTryAcquireShared 方法
完整版的读锁获取流程,for(;😉 保证成功获取锁
final int fullTryAcquireShared(Thread current) {
HoldCounter rh = null;
for (;;) {
int c = getState();// 保存当前 state 值
if (exclusiveCount(c) != 0) {// 判断是否有写锁
// 当前持有写锁的线程不是当前线程,返回 -1,防止死锁发生,此判断表明 读可以升级为写,
if (getExclusiveOwnerThread() != current)
return -1;
} else if (readerShouldBlock()) {// 读线程是否需要阻塞,竞争队列中有等待节点阻塞(公平锁),等待在队列首部的是互斥线程阻塞(非公平锁)
if (firstReader == current) {
} else {
if (rh == null) {
rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current)) {
rh = readHolds.get(); // 获取当前的计数器
if (rh.count == 0)// count 为 0 表示当前线程并未持有锁
readHolds.remove();
}
}
if (rh.count == 0)// 当前现成应该阻塞,并且当前线程不在持有锁,返回 -1 由 AQS 负责其阻塞流程操作
return -1;
}
}
if (sharedCount(c) == MAX_COUNT)
throw new Error("Maximum lock count exceeded");
// 下面的逻辑 同 tryAcquireShared 方法中的 if 条件判断
if (compareAndSetState(c, c + SHARED_UNIT)) {// CAS 获取读锁,并对读锁个数加 1
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; // 缓存最后一个获取读锁的线程
}
return 1;// 告诉 AQS 获取锁成功,让 AQS 唤醒后续等待的读线程
}
}
}
tryReadLock 方法
代码实现同 tryAcquireShared 方法,不同在于该方法不阻塞,获取所失败直接返回
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;
}
}
}
tryReleaseShared 方法
读锁释放流程分析
1、如果是首个获取读锁的线程,直接操作 firstReader 和 firstReaderHoldCount 变量,当 firstReaderHoldCount = 1 时表明重入次数以更新,将 firstReader 置空
2、非首个获取读锁的线程,通过 HoldCounter 操作重入计数信息
protected final boolean tryReleaseShared(int unused) {
Thread current = Thread.currentThread();
if (firstReader == current) { // 判定当前线程是否是首个获取读锁的线程
if (firstReaderHoldCount == 1)// 当等于 1 时,置空 firstReader
firstReader = null;
else
firstReaderHoldCount--;// 重入次数减 1
} else {
HoldCounter rh = cachedHoldCounter;// 保存之前缓存的最后一个获取读锁的 HoldCounter
if (rh == null || rh.tid != getThreadId(current))// 当前线程不是最后一个获取读锁的线程
rh = readHolds.get();// 通过 ThreadLocal 获取当前线程对应的 HoldCounter 信息
int count = rh.count;
if (count <= 1) {
readHolds.remove();// 从 ThreadLocal 中删除 当前线程的 HoldCounter
if (count <= 0)
throw unmatchedUnlockException();
}
--rh.count;// 重入次数减 1
}
// for(;;) 保证读锁释放成功
for (;;) {
int c = getState();
int nextc = c - SHARED_UNIT;
if (compareAndSetState(c, nextc))
return nextc == 0;
}
}
总结:通过上述源码的学习,我们知道了读写锁的实现流程。AQS 提供 state 单变量和 队列的功能,ReentrantReadWriteLock 通过操作这两个信息完成自己的功能。通过将一个整型变量 state 切割为 高低 16 位,分别用于保存读锁和写锁的信息,高 16 位表示读锁,低 16 位表示写锁。因为写锁是互斥的,同一时刻只有一个线程可以获取锁,使用低位直接进行加减操作即可,操作简单;而读锁是共享的,同一时刻可以多个线程持有锁,为了记录每个线程读锁的重入次数,这里引入了 HoldCounter 来记录线程信息和重入次数。为了优化,如果是第一个获取读锁的线程直接通过 firstReader 记录线程信息,firstReaderHoldCount 记录该线程的重入次数,减少对 ThreadLocal 的操作。如果是最后一个获取读锁的线程通过 cachedHoldCounter 缓存 HoldCounter 。