一、写锁加锁解锁源码分析
从宏观上看,ReentrantReadWriteLock (RRWL) 的写锁与 ReentrantLock (RL) 的逻辑非常相似 :它们都是独占锁 、都支持可重入 、都依赖 AQS(AbstractQueuedSynchronizer)实现。具体的实现源码分析可以看ReentrantLock那一篇。
但在底层实现细节和互斥规则 上,两者存在关键区别。主要区别集中在状态位管理 和对读锁的感知上。
以下是详细的对比分析:
1. 核心状态管理(State 的拆分)
这是两者最大的技术差异。AQS 内部维护了一个 int state(32位)来表示锁的状态。
-
ReentrantLock (RL):
-
state 直接表示锁的重入计数。
-
0:未加锁。
-
n > 0:被某个线程持有并重入了 n 次。
-
-
ReentrantReadWriteLock (RRWL):
-
由于一个 state 既要记录读锁又要记录写锁,它采取了**"位拆分"**:
-
高 16 位 :记录读锁的状态(共享锁计数)。
-
低 16 位 :记录写锁的状态(独占锁计数)。
-
逻辑差异:写锁加锁时,需要通过 state & 0x0000FFFF(位掩码)来提取低 16 位,判断写锁的重入次数。
-
2. 加锁逻辑对比 (Acquire)
ReentrantLock 的加锁逻辑:
-
检查 state 是否为 0。
-
如果是 0,尝试 CAS 修改为 1。
-
如果不是 0,检查当前线程是否是持有锁的线程(实现可重入)。
-
否则,进入等待队列。
ReentrantReadWriteLock 写锁的加锁逻辑:
写锁的加锁更加苛刻,它不仅要看有没有别的写锁,还要看读锁:
-
获取当前 state。
-
如果 state != 0:
-
情况 A:写锁计数为 0,但 state 不为 0 。这意味着当前有读锁 被持有。此时写锁加锁失败(写锁被读锁阻塞)。
-
情况 B:写锁计数不为 0 。检查持有写锁的是否是当前线程。如果是,则低 16 位计数递增(可重入);如果不是,加锁失败(写锁被其他写锁阻塞)。
-
-
如果 state == 0:
-
判断是否需要阻塞(受公平策略影响)。
-
尝试 CAS 修改低 16 位加锁。
-
总结差异点 :ReentrantLock 只关心"有没有人拿着锁";ReentrantReadWriteLock 的写锁必须确保"既没有人在读,也没有人在写(除了自己)"。
3. 解锁逻辑对比 (Release)
两者的解锁逻辑基本一致:
-
检查当前线程是否是锁的持有者。
-
对 state(或写锁状态位)进行减法操作。
-
如果减完之后计数为 0,则彻底释放锁,将锁持有者设为 null,并唤醒 AQS 队列中的下一个线程。
微小区别:RRWL 在解锁时同样需要位运算来只操作低 16 位,而 RL 是对整个 int 操作。
二、读锁加锁源码
1. 状态位拆分
RRWL 使用 AQS 的 int state 来同时表示读锁和写锁。它将 32 位的 int 拆分为两部分:
-
高 16 位 :表示读锁(共享锁)持有的次数。
-
低 16 位 :表示写锁(独占锁)重入的次数。
java
static final int SHARED_SHIFT = 16;
static final int SHARED_UNIT = (1 << SHARED_SHIFT); // 0x00010000
static final int MAX_COUNT = (1 << SHARED_SHIFT) - 1;
static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1; // 0x0000FFFF
// 获取读锁数:无符号右移16位
static int sharedCount(int c) { return c >>> SHARED_SHIFT; }
// 获取写锁数:位与掩码
static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }
2.加锁入口
java
public void lock() {
sync.acquireShared(1);
}
3.acquireShared
java
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
4.tryAcquireShared
java
protected final int tryAcquireShared(int unused) {
Thread current = Thread.currentThread();
int c = getState();
//写锁不为0,当前线程没有写锁,不能读
if (exclusiveCount(c) != 0 &&
getExclusiveOwnerThread() != current)
return -1;
int r = sharedCount(c);
//第一个不是写锁节点,读锁没有到最大值,高位+1成功,
if (!readerShouldBlock() &&
r < MAX_COUNT &&
compareAndSetState(c, c + SHARED_UNIT)) {
//第一个读锁
if (r == 0) {
firstReader = current;
firstReaderHoldCount = 1;
//读锁重入
} else if (firstReader == current) {
firstReaderHoldCount++;
} else {
//新来的线程不是第一个读锁的,获取线程缓存计数器
HoldCounter rh = cachedHoldCounter;
//如果不是自己的或者是空,去找自己的ThreadLocal,更新缓存
if (rh == null || rh.tid != getThreadId(current))
cachedHoldCounter = rh = readHolds.get();
else if (rh.count == 0)
//如果是自己的,count为0,为了防止 ThreadLocal 导致内存泄漏,当一个线程的 count 减到 0 时,JDK 会主动调用 readHolds.remove(),所以需要更新本地的映射关系
readHolds.set(rh);
//最后缓存计数器的count+1
rh.count++;
}
return 1;
}
//如果第一个是写锁,执行fullTryAcquireShared
return fullTryAcquireShared(current);
}
5.readerShouldBlock()
java
final boolean readerShouldBlock() {
//第一个等待节点是写锁,返回true
return apparentlyFirstQueuedIsExclusive();
}
}
final boolean apparentlyFirstQueuedIsExclusive() {
Node h, s;
return (h = head) != null && // 1. 队列头节点不为空(说明队列已初始化)
(s = h.next) != null && // 2. 存在第一个等待中的节点(head.next)
!s.isShared() && // 3. 该节点不是共享模式(即:它是独占模式,想加写锁)
s.thread != null; // 4. 该节点的线程不为空(确保该节点有效)
}
6.fullTryAcquireShared
java
final int fullTryAcquireShared(Thread current) {
HoldCounter rh = null;
for (;;) {
int c = getState();
if (exclusiveCount(c) != 0) {
//有人持有写锁并且不是自己的,不能抢读锁(如果是自己的就直接去抢锁,这叫锁降级)
if (getExclusiveOwnerThread() != current)
return -1;
//第一个是写节点,需要阻塞,但需要确保读锁是不是重入的
} else if (readerShouldBlock()) {
if (firstReader == current) {
// 你是第一个获取到读锁的,你肯定持有锁,这是重入,重入大于一切,(具体原因看下面讲解)【不能阻塞】,直接跳过这个 if 去抢锁
} else {
//不是第一个,也要判断一下你有没有获取过读锁
if (rh == null) {
rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current)) {
rh = readHolds.get();
if (rh.count == 0)
readHolds.remove();
}
}
//获取count,确定是个新人,不是重入锁,阻塞
if (rh.count == 0)
return -1;
}
}
//到达这里的都是可以抢锁的,包括锁降级和锁重入
if (sharedCount(c) == MAX_COUNT)
throw new Error("Maximum lock count exceeded");
//state改变成功
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; // cache for release
}
return 1;
}
}
}
为什么重入大于一切?
我们来看一个死锁发生的经典剧本:
-
背景:线程 A 已经持有了读锁(Read Lock)。
-
冲突:线程 B 请求写锁(Write Lock)。由于线程 A 没放手,线程 B 进入 AQS 队列排队,并成为了队列的头节点(Head.next)。
-
策略生效:此时,读写锁的策略(readerShouldBlock())为了不让线程 B 饿死,会返回 true,意思是有写者在排队,后续读者请自觉去排队。
-
致命重入:此时线程 A 在执行业务逻辑时,又调用了一次 lock() 请求重入。
-
假如"策略大于重入":
-
线程 A 看到策略说"请排队",于是线程 A 跑去队列里排在线程 B 后面。
-
现在的局面:
-
线程 A 在等 线程 B(因为它在队列里排在 B 后面)。
-
线程 B 在等 线程 A(因为它要拿写锁,必须等 A 释放掉手中已有的读锁)。
-
-
结局 :死锁。系统所有功能卡死,只能重启。
-
所以为了避免死锁出现,重入优先,对于已经
7.tryAcquireShared返回
能抢到锁就返回1,不能抢锁就返回-1
8.没抢到锁就执行doAcquireShared,直到获取到锁
java
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
java
private void doAcquireShared(int arg) {
//入队
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head) {
//第一个等待节点,可以获取锁
int r = tryAcquireShared(arg);
if (r >= 0) {
//更新头结点,A 拿到了读锁,设置自己为 head 之后,会检查后面的人是不是也在等读锁。如果是,顺便把后面的人也叫醒。
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);
}
}
三、读锁解锁源码分析
1.解锁入口
java
public void unlock() {
sync.releaseShared(1);
}
2.releaseShared
java
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
3.tryReleaseShared
java
protected final boolean tryReleaseShared(int unused) {
Thread current = Thread.currentThread();
//是第一个读锁
if (firstReader == current) {
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;
//count=1,直接清理ThreadLocal
if (count <= 1) {
readHolds.remove();
if (count <= 0)
throw unmatchedUnlockException();
}
//count大于1
--rh.count;
}
for (;;) {
int c = getState();
int nextc = c - SHARED_UNIT;
if (compareAndSetState(c, nextc))
return nextc == 0;
}
}
tryReleaseShared会返回释放锁之后state是不是0,如果是,就返回true,不是就返回false。
4.releaseShared
java
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
tryReleaseShared如果返回true,证明现在读锁锁完全空闲,执行 doReleaseShared();唤醒其他等待节点。
为什么是返回state,因为你既然去释放的是读锁,证明肯定没有写锁,所以你只需要判断读锁有没有完全释放,只有读锁完全释放了,state才能是0,才能去唤醒队列里的写锁。
java
private void doReleaseShared() {
for (;;) {
Node h = head;
if (h != null && h != tail) {
int ws = h.waitStatus;
//有等待节点,唤醒后续节点
if (ws == Node.SIGNAL) {
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue;
unparkSuccessor(h);
}
//它是一个特殊的"传播"状态。设置这个状态,相当于留下一个标记:"我是一个共享锁的释放操作,请确保这个唤醒信号能继续向后传播下去!"(下面有具体解释)
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue;
}
//队列状态没变,结束循环
if (h == head)
break;
}
}
-
ws == 0 的场景:当一个节点刚刚成为 head 时,它的 waitStatus 可能是 0。此时,如果一个释放操作和一个加锁操作(新线程入队)同时发生,可能会导致唤醒信号"断链"。
-
PROPAGATE (-3) 的作用 :它是一个特殊的"传播"状态。设置这个状态,相当于留下一个标记:"我是一个共享锁的释放操作,请确保这个唤醒信号能继续向后传播下去!"
-
为什么需要它?
-
线程 A 释放最后一个读锁,开始执行 doReleaseShared。
-
它唤醒了线程 B(读者)。线程 B 成功拿到锁,并成为新的 head。
-
在 B 成为 head 的一瞬间,线程 C(读者)也来排队,挂在了 B 后面。
-
此时,线程 A 的 doReleaseShared 可能还没结束。它看到新的 head(即 B)的 waitStatus 是 0,如果不做任何事,唤醒链就可能到此为止,C 就醒不过来了。
-
通过 compareAndSetWaitStatus(h, 0, Node.PROPAGATE),线程 A 强制给新的 head(B)打上了一个"请继续传播"的标记。这样,当 B 拿到锁后,它看到 head 状态是 PROPAGATE,就知道自己有责任继续唤醒 C。
-
java
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
//后续有等待节点
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
Node s = node.next;
//已经取消等待,节点失效
if (s == null || s.waitStatus > 0) {
s = null;
//遍历,找到第一个有效节点
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
if (s != null)
LockSupport.unpark(s.thread);
}
5.releaseShared返回
java
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
只有state为0,证明已经完全释放读锁,并且唤醒后续节点,releaseShared返回true.
如果没有完全释放锁,就返回false.
四、读锁的加锁解锁总结
加锁步骤
第 1 阶段:快速路径 (tryAcquireShared)
- 判断是否有写锁:
-
是,且不是我:失败,转入第 2 阶段。
-
是,且是我(锁降级):通过,继续。
-
否:通过,继续。
- 判断是否需要阻塞(readerShouldBlock())
上一步判断了没有写锁,为什么阻塞的时候要再判断写锁在队首?
因为如果不判断直接加锁,那么在队首的写者就会一直等待,导致写饥饿
-
是(公平锁有人排队 / 非公平锁写者在队首):失败,转入第 2 阶段。
-
否:通过,继续。
- CAS 尝试
-
成功 :加锁成功! 流程结束。
-
失败(有竞争):转入第 2 阶段。
第 2 阶段:完整路径 (fullTryAcquireShared)
-
死循环开始:
-
写锁冲突检查:同第 1 阶段,有写锁且不是自己,则返回 -1 去排队。
-
策略与死锁检查(核心):再次询问 readerShouldBlock()。
-
如果返回 false(无需阻塞):跳过此检查,直接去 CAS。
-
如果返回 true (建议阻塞):必须进行"身份政审"以防死锁。
-
你是 firstReader 吗?
- 是 :无视阻塞建议 ,跳过此检查,去 CAS。(重入 > 一切)
-
你不是 firstReader,查你的账本 HoldCounter:
-
count > 0 :你也是"重入",无视阻塞建议,去 CAS。
-
count == 0 :你是"新客户",必须遵守规则,返回 -1 去排队。
-
-
-
-
CAS 尝试 :同第 1 阶段,如果 CAS 成功,则加锁成功并返回。如果失败,则继续死循环,从头再来。
第 3 阶段:排队等待 (doAcquireShared)
如果第 2 阶段返回了 -1,说明必须排队。
-
入队:将自己打包成 SHARED 节点,挂到队尾。
-
自旋等待:
-
检查位置:我的前面是 head 吗?
-
是:说明轮到我了,再次尝试 tryAcquireShared。
-
成功 :将自己设为新 head,并传播唤醒 后面的读者(setHeadAndPropagate)。加锁成功!
-
失败:继续下面的步骤。
-
-
否:继续下面的步骤。
-
-
准备睡觉:检查并设置前驱节点的状态为 SIGNAL。
-
执行睡觉:调用 park() 挂起线程,等待被唤醒。
-
解锁步骤
第 1 阶段:读者自己的结算 (tryReleaseShared - 前半部分)
-
身份判定:我是 firstReader 吗?
-
是:操作 firstReaderHoldCount 成员变量。
-
count == 1:说明是最后一层锁,将 firstReader 设为 null。
-
count > 1:只是重入,count--。
-
-
否:说明我是其他读者,操作 ThreadLocal 里的 HoldCounter。
-
count <= 1 :说明即将彻底释放,必须调用 readHolds.remove() 防止内存泄漏。
-
count > 1:只是重入,count--。
-
count <= 0:非法操作,抛出异常。
-
-
第 2 阶段:全局状态state更新与信号判断 (tryReleaseShared - 后半部分)
-
自旋 CAS:进入死循环,确保能成功更新全局 state。
-
计算新状态:nextc = c - SHARED_UNIT。
-
执行 CAS:compareAndSetState(c, nextc)。
-
发出信号(核心):CAS 成功后,执行 return nextc == 0;。
-
返回 false :nextc 不为 0,说明还有其他读者。AQS 收到后什么也不做。
-
返回 true :nextc 为 0,说明我是最后一个读者 。这是一个质变信号。AQS 收到后会执行 doReleaseShared,去唤醒队列里等待的线程。
-