文章目录
学习 ReentrantReadWriteLock 需要重点关注以下四点:
- 写写互斥、写读互斥,读读不互斥;共享锁适用于读多写少的场景。
- state 变量拆分为高 16 位和低 16 位两部分来使用。
- 使用 ThreadLocal 保存每个读锁的重入次数。
- readerShouldBlock 如何保证写线程不饥饿(写线程一直获取不到锁的情况)。
用法
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class CachedData {
String data;
volatile boolean cacheValid;
final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
public void processCachedData(String newData) {
rwl.readLock().lock(); // 上读锁
if (!cacheValid) { // 缓存无效
rwl.readLock().unlock(); // 释放读锁
rwl.writeLock().lock(); // 获取写锁(写操作和其他操作互斥,所以要先释放读锁)
try {
if (!cacheValid) {
data = newData; // 更新缓存
cacheValid = true; // 更新缓存标志位
}
rwl.readLock().lock(); // 获取读锁(此时同时拥有写锁和读锁)
} finally {
rwl.writeLock().unlock(); // 释放写锁,锁降级为读锁
}
}
try {
System.out.println("data = " + data);
} finally {
rwl.readLock().unlock(); // 释放读锁
}
}
public static void main(String[] args) {
new CachedData().processCachedData("testRwl");
}
}
输出:
data = testRwl
核心源码
Sync
Sync 类继承了 AQS,这也是个[[模版方法模式]],实现了公平锁和非公平锁公共的方法,同时定义了一些抽象方法,让 FairSync 和 NonfairSync 子类分别去实现,比如 readerShouldBlock
这个方法。
abstract static class Sync extends AbstractQueuedSynchronizer {
// 每个线程持有的共享锁的数量
static final class HoldCounter {
// 读锁的重入次数
int count = 0;
// 使用线程 ID 而不是 Thread 对象的引用,避免在 GC 时持有线程对象的引用
final long tid = getThreadId(Thread.currentThread());
}
// 因为读线程有多个,需要统计每个读线程的重入次数,所以要使用 ThreadLocal 来记录每个线程自己的读锁重入次数。
// 这里是个优化点:如果只有一个读线程,即第一个读线程。那么直接保存在 firstReader 中就好了,省去了 ThreadLocal 的开销。
static final class ThreadLocalHoldCounter
extends ThreadLocal<HoldCounter> {
public HoldCounter initialValue() {
return new HoldCounter();
}
}
private transient ThreadLocalHoldCounter readHolds;
// 缓存最后一个成功获取共享锁的线程的计数器
private transient HoldCounter cachedHoldCounter;
// 缓存第一个获取读锁的线程和它的读锁计数器
private transient Thread firstReader = null;
private transient int firstReaderHoldCount;
Sync() {
readHolds = new ThreadLocalHoldCounter();
setState(getState());
}
// 用于判断当前获取共享锁的线程是否应该被阻塞,由子类实现
abstract boolean readerShouldBlock();
// 用于判断当前获取互斥锁的线程是否应该被阻塞,由子类实现
abstract boolean writerShouldBlock();
}
state
Sync 类通过二进制截断将 AQS 中的 state 变量分割为了两个无符号 16 位二进制变量。
高 16 位用于表示持有读锁的线程的数量,低 16 位用于表示持有写锁的线程的重入次数。如下所示:
0000000000000000(持有读锁的线程数量) 0000000000000000(持有写锁的线程的重入次数)
注:同一时刻允许有个线程持有读锁,读锁也可以重入,那不同读锁的重入次数怎么办?读锁的重入次数记录在 ThreadLocal 中。
// 共享锁偏移量
static final int SHARED_SHIFT = 16;
// 获取 c 的高 16 位,即当前线程持有读锁的数量
// 获取高 16 位直接无符号右移即可
static int sharedCount(int c) { return c >>> SHARED_SHIFT; }
// 1 左移 16 位减一,得到一个二进制高 16 位都是 0,低 16 位都是 1
// 00000000000000001111111111111111
static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;
// 互斥锁最大重入次数
static final int MAX_COUNT = (1 << SHARED_SHIFT) - 1;
// 获取 c 的低 16 位,即当前线程持有写锁的数量
// &:二进制截断
// EXCLUSIVE_MASK:00000000000000001111111111111111
static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }
NonfairSync extends Sync
static final class NonfairSync extends Sync {
final boolean writerShouldBlock() {
// 非公平情况下,写线程永远不需要阻塞
return false;
}
final boolean readerShouldBlock() {
// 为了避免读线程不断获取锁,导致写线程饥饿(写线程获取不到锁)。此时要判断队列中第一个等待的结点是否是写线程,如果是则阻塞读线程
return apparentlyFirstQueuedIsExclusive();
}
}
apparentlyFirstQueuedIsExclusive 方法
final boolean apparentlyFirstQueuedIsExclusive() {
Node h, s;
return (h = head) != null && // head 不为 null
(s = h.next) != null && // head.next 不为 null
!s.isShared() && // head.next 不是读线程
s.thread != null; // head.next 的 thread 不为 null
// 以上条件都满足的读线程需要进队列排队,避免写线程饥饿
}
FairSync extends Sync
不管是读线程还是写线程,都需要查看队列中是否有线程在等待,若有,则需要阻塞。
static final class FairSync extends Sync {
final boolean writerShouldBlock() {
return hasQueuedPredecessors();
}
final boolean readerShouldBlock() {
return hasQueuedPredecessors();
}
}
hasQueuedPredecessors
public final boolean hasQueuedPredecessors() {
Node t = tail; // Read fields in reverse initialization order
Node h = head;
Node s;
// h != t 队列不为空
// h.next 为 null
// h.next 不为 null,且不是当前线程
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
WriteLock#lock 获取写锁
WriteLock 的 lock 方法调用了 AQS 的 acquire 方法。
public static class WriteLock implements Lock, java.io.Serializable {
private final Sync sync;
protected WriteLock(ReentrantReadWriteLock lock) {
sync = lock.sync;
}
public void lock() {
sync.acquire(1);
}
}
acquire 方法
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) // Node.EXCLUSIVE表示互斥锁
selfInterrupt(); // 如果在等待过程中发生中断,传递中断标志位
}
acquire 方法依赖于 tryAcquire 方法的返回值,tryAcquire 方法的具体逻辑由 AQS 子类实现。
- 若 tryAcquire 方法返回值为 false,则先将当前线程封装为 Node 对象,插入同步队列中。
- 然后判断当前线程是否应该阻塞等待,通过死循环保证当前线程阻塞等待或者发生异常被取消或者直接获得锁。
- 如果在等待过程中发生中断,则传递中断标志位
tryAcquire
实现 AQS 的 tryAcquire 方法。
protected final boolean tryAcquire(int acquires) {
Thread current = Thread.currentThread();
int c = getState();
// 当前线程持有互斥锁的数量
int w = exclusiveCount(c);
if (c != 0) {
// c 的高 16 位表示线程持有读锁的数量,低 16 位表示线程持有写锁的数量
// c != 0 and w == 0:说明线程持有读锁,读写互斥,直接返回 false
// c != 0 and w != 0:说明有线程持有写锁,此时需要判断持有写锁的线程是否是当前线程,若不是当前线程,直接返回 false
// 返回 false 后,线程会进入 AQS 队列排队
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;
}
// writerShouldBlock 抽象方法,由子类实现
// 判断写线程是否应该阻塞
// 若不应该阻塞,则尝试获取写锁,若获取失败直接返回false,进入AQS中排队
if (writerShouldBlock() ||
!compareAndSetState(c, c + acquires))
return false;
// 若获取写锁成功,则将当前线程设置为持有锁的线程
setExclusiveOwnerThread(current);
return true;
}
WriteLock#unlock 释放写锁
WriteLock 的 unlock 方法调用了 AQS 的 release 方法。
public void unlock() {
sync.release(1);
}
release 释放互斥锁
Releases in exclusive mode. Implemented by unblocking one or more threads if tryRelease returns true. This method can be used to implement method Lock#unlock.
public final boolean release(int arg) {
if (tryRelease(arg)) {
// 头结点也即当前结点
Node h = head;
// 若当前结点的 waitStatus 状态不等于 0,则唤醒后继结点
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
tryRelease
重写 AQS 的 tryRelease 方法。
protected final boolean tryRelease(int releases) {
// 判断当前线程是否持有写锁,若没持有,则抛异常
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
// 因为锁可重入,所以需要判断当前线程是否完全释放完写锁
// 释放锁时已经持有锁,所以下述操作不需要 CAS
int nextc = getState() - releases;
boolean free = exclusiveCount(nextc) == 0;
if (free)
setExclusiveOwnerThread(null);
setState(nextc);
// 若 free 为 true,则说明当前线程完全释放了写锁。此时会通知 AQS 唤醒后面的等待线程
return free;
}
ReadLock#lock 获取读锁
ReadLock 的 lock 方法调用了 AQS 的 acquireShared 方法。
public static class ReadLock implements Lock, java.io.Serializable {
private final Sync sync;
protected ReadLock(ReentrantReadWriteLock lock) {
sync = lock.sync;
}
public void lock() {
sync.acquireShared(1);
}
}
acquireShared 方法
acquireShared 和 acquire 主要区别是,当前线程获取锁后是否会唤醒后继结点。
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
tryAcquireShared
实现 AQS 的 tryAcquireShared 方法。
protected final int tryAcquireShared(int unused) {
Thread current = Thread.currentThread();
int c = getState();
// 若已有线程持有写锁,且不是当前线程,则直接返回 -1
if (exclusiveCount(c) != 0 &&
getExclusiveOwnerThread() != current) // 支持锁降级,即获取写锁的线程还能获取读锁,然后再释放写锁,就完成了锁降级
return -1;
// 通过 state 获取共享锁持有的数量
int r = sharedCount(c);
// 通过子类判断当前读线程是否应该被阻塞(避免写线程饥饿)
if (!readerShouldBlock() &&
r < MAX_COUNT && // 若不阻塞,则需要校验持有共享锁的数量是否达到最大值
compareAndSetState(c, c + SHARED_UNIT)) { // 获取的共享锁的数量加 1
if (r == 0) { // 当前持有共享锁的数量为 0,那么此线程是第一个持有共享锁的线程
// 保存到 firstReader 中
firstReader = current;
firstReaderHoldCount = 1;
} else if (firstReader == current) { // 锁重入
firstReaderHoldCount++;
} else { // 不是第一个持有共享锁的线程,则从 TL 中取出 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;
}
// 完整版本获取共享锁
return fullTryAcquireShared(current);
}
doAcquireShared 方法
private void doAcquireShared(int arg) {
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
// 若当前结点的前继结点是头节点,则调用子类实现的 tryAcquireShared 获取锁
if (p == head) {
// 若成功获取锁,则设置当前结点为头节点,并唤醒同样等待在 SHARED 模式下的线程
// ReentrantReadWriteLock 的实现中返回值为 1 表示获取到读锁,-1 表示未获取到
int r = tryAcquireShared(arg);
if (r >= 0) {
setHeadAndPropagate(node, r);
p.next = null; // help GC
if (interrupted)
selfInterrupt(); // 若被中断过,传递中断标志位
failed = false;
return;
}
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
setHeadAndPropagate 方法唤醒其他读线程
Sets head of queue, and checks if [[successor]] may be waiting in shared mode, if so propagating if either propagate > 0 or PROPAGATE status was set.
private void setHeadAndPropagate(Node node, int propagate) {
Node h = head; // 保存旧的 head 结点
setHead(node); // 当前结点设置为头结点
// 老爷子的习惯,在使用变量之前都会做非空校验,虽然这里 h 和 head 必不为 null,但是还是校验了 h == null 和 (h = head) == null
// 下述条件判断可简化为 if(propagate > 0 || h.waitStatus < 0 || head.waitStatus < 0)
if (propagate > 0 || h == null || h.waitStatus < 0 ||
(h = head) == null || h.waitStatus < 0) {
Node s = node.next;
if (s == null || s.isShared()) // 此处 s 也不可能为 null,所以可简写为 if(s.isShared())。下一个结点是读线程才唤醒,写线程就不唤醒了
doReleaseShared();
}
}
ReentrantReadWriteLock 中 tryAcquireShared 的返回值只有 1 和 -1 两种,若获取到读锁,返回值为 1,则 propagate 为 1,那么 propagate > 0 成立,其他条件都不用看,setHeadAndPropagate 可简写为:
private void setHeadAndPropagate(Node node, int propagate) {
Node h = head;
setHead(node);
Node s = node.next;
if (s.isShared())
doReleaseShared();
}
fullTryAcquireShared
final int fullTryAcquireShared(Thread current) {
HoldCounter rh = null;
for (;;) {
int c = getState();
if (exclusiveCount(c) != 0) {
if (getExclusiveOwnerThread() != current)
return -1; // 持有锁的线程不是当前线程,直接返回 -1
} else if (readerShouldBlock()) { // 获取读锁的线程是否应该阻塞
if (firstReader == current) {
} else { // 当前线程不是第一个获取共享锁的线程,从 TL 中获取计数器
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) // 当前线程应该阻塞且不持有读锁,返回 -1,由 AQS 安排阻塞
return -1;
}
}
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; // 缓存最后一个获取到共享锁的线程
}
return 1; // 通知 AQS 唤醒后续阻塞的读线程
}
}
}
ReadLock#unLock 释放读锁
ReadLock 的 unLock 方法调用了 AQS 的 releaseShared 方法。
public void unlock() {
sync.releaseShared(1);
}
releaseShared
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
tryReleaseShared
实现了 AQS 的 tryReleaseShared 方法。
protected final boolean tryReleaseShared(int unused) {
Thread current = Thread.currentThread();
// 当前线程是第一个获取锁的线程
if (firstReader == current) {
// 当前线程只持有一个共享锁,直接释放掉
if (firstReaderHoldCount == 1)
firstReader = null;
else
firstReaderHoldCount--; // 重入次数减 1
} else { // 当前线程不是第一个获取到读锁的线程
HoldCounter rh = cachedHoldCounter;
// 最后一个持有读锁的线程为 null 或者当前线程不是最后一个持有读锁的线程
if (rh == null || rh.tid != getThreadId(current))
rh = readHolds.get(); // 从 TL 中获取到当前的线程的读锁计数器
int count = rh.count;
// 如果计数器小于等于 1,则移除当前线程对 HoldCounter 的引用
if (count <= 1) {
readHolds.remove();
if (count <= 0)
throw unmatchedUnlockException();
}
// 重入次数减 1
--rh.count;
}
// 更新完线程自己的重入计数器后,还需要修改全局的读锁计数器
// 死循环 CAS 更新 state
for (;;) {
int c = getState();
// 读锁数量减 1
// c 的高 16 位用于表示线程持有读锁的数量
// 0000 0000 0000 0001 0000 0000 0000 0000
int nextc = c - SHARED_UNIT;
if (compareAndSetState(c, nextc))
return nextc == 0; // 如果没有读线程了,通知 AQS 唤醒队列中阻塞的写线程
}
}