上文介绍了适用于读操作远多于写操作的读写锁。
ReentrantReadWriteLock 的读锁虽然是共享的,但获取读锁本身是有开销的,又是 CAS 操作,又是 记录线程重入,又是 AQS 队列管理的,而且获取读锁期间写线程会被完全阻塞。如果一个方法几乎只读、极少写,每次读都走一遍读锁的完整流程其实有点浪费。
本篇文章再介绍一种适应于读操作远远多于写操作的锁 —— StampedLock。
StampedLock 通过 UML 可以看出也属于 Java 中锁的体系结构,包含两个视图类,读锁视图(ReadLockView)和写锁视图(WriteLockView);好像还没有使用 AQS。
对于源码分析先放一放,先简单介绍一下这个锁的定位。
StampedLock 是一种乐观锁,先假设数据没有变化,先读了再说,先乐观地读,不加锁,如果读出来的数有变化,再降级为读锁重新读。
前面讲的 原子类型 也是乐观锁,它也是假设数据没有变化,先更新了再说,更新失败说明数据有过变化,原子类是拿 “预期值” 判断数据有没有变化的,StampedLock 也一样,只不过 StampedLock 比较数据有没有变化的 “预期值” 是版本号戳 —— Stamp 。
乐观锁:先假设没有冲突直接操作,再通过“预期值”验证数据是否变化,失败则重试或降级。
悲观锁:它假设冲突一定会发生,操作前先独占资源,阻塞其他线程,直到自己完成并释放锁。(宁可错杀一千,不可放过一个)
下面就先介绍一下 StampedLock 的基本使用,再用 ReentrantLock 搞一个简单版的 StampedLock ,最后再看看并发编程大神Doug Lea(道格·利) 写的源码。
基本使用
ReadWriteLock 支持两种模式:一种是读锁,一种是写锁;而 StampedLock 提供三种访问模式:
| 模式 | 方法 | 语义 |
|---|---|---|
| 乐观读 | tryOptimisticRead() | 返回一个 stamp,不阻塞,不排斥任何读,但读取后需 validate(stamp) 验证 |
| 读锁 | readLock() / unlockRead() | 传统共享读锁,排斥写锁,支持重入 |
| 写锁 | writeLock() / unlockWrite() | 独占写锁,排斥所有读和写 |
StampedLock 的 读锁、写锁 和 ReadWriteLock 的 读锁、写锁 差不多,都是共享读、独占写,只是 StampedLock 要 通过 stamp 来加锁和解锁,加锁方法会返回 stamp,解锁方法也需要这个 stamp。
// 乐观读:返回一个版本号戳,不阻塞
long stamp = lock.tryOptimisticRead();
// 验证:如果期间没有写操作,返回 true
boolean valid = lock.validate(stamp);
// 读锁(悲观):如果存在写锁则阻塞等待
long readStamp = lock.readLock();
try { ... } finally { lock.unlockRead(readStamp); }
// 写锁(独占)
long writeStamp = lock.writeLock();
try { ... } finally { lock.unlockWrite(writeStamp); }
经典使用模式
class Point {
private double x, y;
private final StampedLock sl = new StampedLock();
// 乐观读:先读,后验证
public double distanceFromOrigin() {
long stamp = sl.tryOptimisticRead(); // 1. 乐观读
double currentX = x, currentY = y; // 2. 读取数据
if (!sl.validate(stamp)) { // 3. 验证版本
// 验证失败,说明期间有写操作,降级为读锁重试
stamp = sl.readLock(); // 4. 获取悲观读锁
try {
currentX = x; // 5. 重新读取
currentY = y;
} finally {
sl.unlockRead(stamp); // 6. 释放读锁
}
}
return Math.sqrt(currentX * currentX + currentY * currentY);
}
// 写操作:独占
public void move(double deltaX, double deltaY) {
long stamp = sl.writeLock(); // 1. 获取写锁
try {
x += deltaX;
y += deltaY;
} finally {
sl.unlockWrite(stamp); // 2. 释放写锁
}
}
}
StampedLock 适应于 读操作远远多于写操作 的场景,所以绝大多数情况下 validate 直接返回 true,整个读操作没有获取任何锁,性能接近无锁。
只有在写操作频繁的场景下才会触发降级逻辑。
读锁升级
对于 ReentrantReadWriteLock 来说,仅支持从写锁降级为读锁,这是因为写锁是独占的,同一时刻只有一个线程持有,该线程在持有写锁期间再去获取读锁,不存在其他并发读线程。
但 ReentrantReadWriteLock 明确禁止从读锁升级为写锁,因为读锁是共享的。假如线程 A 和线程 B 同时持有读锁,同时想要升级到写锁,但线程 A 想要升级到写锁需要等待 线程 B 的读锁释放,而线程 B 想要升级到写锁又需要等待线程 A 的读锁释放,循环等待,死锁了。
但 StampedLock 就比较灵活, 支持锁升级(乐观读 → 读锁 → 写锁),至于为啥它就没并发问题,后面讲解源码的时候再谈。
public void updateIfFar(Point p, double threshold) {
long stamp = sl.tryOptimisticRead();
double dist = Math.sqrt(p.x * p.x + p.y * p.y);
if (!sl.validate(stamp)) {
stamp = sl.readLock();
boolean needUnlockRead = true; // 标记:是否还需要释放读锁
try {
dist = Math.sqrt(p.x * p.x + p.y * p.y);
if (dist > threshold) {
long writeStamp = sl.tryConvertToWriteLock(stamp);
if (writeStamp != 0L) {
// 读锁原子转为写锁,旧读锁已消耗,不需要再unlockRead
needUnlockRead = false;
try {
// write操作
} finally {
sl.unlockWrite(writeStamp);
}
} else {
// 升级失败,手动释放读锁,拿写锁
needUnlockRead = false;
sl.unlockRead(stamp);
long ws = sl.writeLock();
try {
// write操作
} finally {
sl.unlockWrite(ws);
}
}
return;
}
} finally {
if (needUnlockRead) {
// 只有读锁还持有时,才释放读锁
sl.unlockRead(stamp);
}
}
}
}
注意 tryConvertToWriteLock(stamp) 这个方法是关键——它把已有的读锁直接升级为写锁,比先释放读锁再重新获取写锁要高效得多,避免了中间态的竞态窗口。
源码解析
通过上面的 UML 类图也可以看出来,StampedLock 内部没有 继承 AQS 的 Sync,没有 AQS 的帮助,那它是怎么实现锁机制的呢?
我们知道 AQS 最重要的是记录线程状态的 state 和 存放竞争失败线程的阻塞队列,那 StampedLock 是怎么实现这两个功能的呢?
public class StampedLock implements java.io.Serializable {
/** The number of bits to use for reader count before overflowing */
private static final int LG_READERS = 7; // 127 readers
// Values for lock state and stamp operations
private static final long RUNIT = 1L;
private static final long WBIT = 1L << LG_READERS;
private static final long RBITS = WBIT - 1L;
private static final long RFULL = RBITS - 1L;
private static final long ABITS = RBITS | WBIT;
private static final long SBITS = ~RBITS; // note overlap with ABITS
// not writing and conservatively non-overflowing
private static final long RSAFE = ~(3L << (LG_READERS - 1));
private static final long ORIGIN = WBIT << 1;
// Special value from cancelled acquire methods so caller can throw IE
private static final long INTERRUPTED = 1L;
// Bits for Node.status
static final int WAITING = 1;
static final int CANCELLED = 0x80000000;
abstract static class Node {
volatile Node prev; // initially attached via casTail
volatile Node next; // visibly nonnull when signallable
Thread waiter; // visibly nonnull when enqueued
volatile int status;
}
private transient volatile Node head;
/** Tail (last) of CLH queue */
private transient volatile Node tail;
// views
transient ReadLockView readLockView;
transient WriteLockView writeLockView;
transient ReadWriteLockView readWriteLockView;
private transient volatile long state;
private transient int readerOverflow;
public StampedLock() {
state = ORIGIN;
}
锁状态 state
看成员变量就知道, StampedLock 也有一个 volatile 修饰的 long 类型的 state(64 位),比 AQS int 类型的 state(32 位) 长了两倍。
对于 ReentrantLock 来说,state 代表重入计数,对于 ReentrantReadWriteLock 来说,高 16 位表示读状态(所有持有读锁线程的总重入次数),低 16 位表示写状态(写锁持有线程的重入次数),而 StampedLock 的 state 也承担相似的功能:
| 位范围 | 含义 |
|---|---|
| 低 16 位 | 写锁等待线程数(writer count) |
| 中间 16 位 | 读锁等待线程数(reader count) |
| 高 32 位 | stamp 版本号(每次写操作后递增) |
正是因为它需要用一个单一的变量同时承载版本号、读写状态和读锁计数这三种信息,所以用了位数更长的 long,而不是 int。
| 比特区域 | 含义 |
|---|---|
| bit63 ~ bit8 | stamp(版本戳,每次写锁释放会自增) |
| bit7 | WBIT:写锁标志位,1 = 持有写锁 |
| bit6 ~ bit0(低 7bit) | 悲观读锁计数 0~127 |
再看 StampedLock 这么多的 静态 long 常量,都是在给 state 这个 64 位 long 做“位域划分”:
/** 读锁相关 **/
// 给读锁计数预留 7 个 bit,所以最多0~127个悲观读
private static final int LG_READERS = 7; // 127 readers
// 读锁的“增量单位”。每多一个线程拿到读锁,就是 next = s + RUNIT,即 state 低 7 位加 1。
private static final long RUNIT = 1L;
// 值为 127,二进制 01111111,是读锁的掩码。用 s & RBITS 就能把 state 的低 7 位单独取出来。
private static final long RBITS = WBIT - 1L;
// 值为 126,二进制 01111110。它是个“接近满”的阈值,用于判断 读锁计数是否快溢出了,防止加到 128 溢出侵占 WBIT 位。
private static final long RFULL = RBITS - 1L;
/** 写锁相关 **/
// 即 128,二进制 10000000,是写锁标志位。获取写锁时相当于给 state 加上这个值。
private static final long WBIT = 1L << LG_READERS;
// 值为 255,二进制 11111111,是“所有锁位”的合集掩码,用于判断当前有没有读锁和写锁。
private static final long ABITS = RBITS | WBIT;
// 把低 7 位全部屏蔽掉,只保留写锁位和更高位的版本号区域。
private static final long SBITS = ~RBITS; // note overlap with ABITS
/** 其他 **/
// 保守的安全掩码,用于判断“当前没有写锁、且读计数还没逼近溢出”的状态,避免在临界值附近做乐观判断。
private static final long RSAFE = ~(3L << (LG_READERS - 1));
// 值为 256,二进制 100000000,是 state 的初始值。此时写锁位和读锁位都是 0;之所以不从 0 开始,是因为 0 在 StampedLock 的语义里表示“操作失败”。
private static final long ORIGIN = WBIT << 1;
// 一个特殊的返回值,由被取消的获取方法返回,用来让调用方知道该抛出 InterruptedException。
private static final long INTERRUPTED = 1L;
还有一个细节state 只有 1 个 bit(bit7,WBIT)标记是否持有写锁,没有任何字段 /bit 用来记录写锁的重入次数,也没有 owner 记录当前线程,可见写锁不支持重入。
同样的,悲观读锁虽然有 7bit 计数,但计数是统计并发读线程数量,不是单线程重入次数,而且也不像 ReentrantReadWriteLock 那样使用各自读线程的 LocalThread 记录重入次数,per-thread 的持有计数,所以读锁也不支持重入,这也是为什么 StampedLock 不叫 ReentrantStampedLock 。
阻塞队列 CLH
abstract static class Node {
volatile Node prev; // initially attached via casTail
volatile Node next; // visibly nonnull when signallable
Thread waiter; // visibly nonnull when enqueued
volatile int status;
}
private transient volatile Node head;
private transient volatile Node tail;
结构和 AQS 的阻塞队列差不多,都是CLH 变体双向等待队列。
新来的抢锁失败的线程,就是通过 CAS 操作,把自己包装成新的 Node,接到 tail 的后面,然后把自己变成新的 tail。
下面直接看乐观锁的获取与验证。
乐观读 tryOptimisticRead
public long tryOptimisticRead() {
long s;
// `s & WBIT == 0`:检查此刻没有写锁**(bit7 写标记为 0)
// 无写锁:返回版本戳 stamp
// 有写锁:直接返回 `0L`
return (((s = state) & WBIT) == 0L) ? (s & SBITS) : 0L;
}
public boolean validate(long stamp) {
// 加载屏障,保证屏障之后的内存读取,不能被重排到屏障之前。
U.loadFence();
// 清除低 7 位悲观读计数,只比较版本戳区域(bit7~bit63)
return (stamp & SBITS) == (state & SBITS);
}
乐观读,读取 state 时,如果当前无写锁,返回抹除读计数后的版本戳;有写锁直接返回 0,0 代表乐观读失败,也这是为什么 初始 state 是 不是 0,如果是 0,就无法区分 “锁空闲” 和 “乐观读失败” 两种情况。
validate 验证版本号,内部带 Unsafe 读屏障,它约束的是屏障前后的读操作顺序,防止把后面读取业务字段的动作重排到屏障之前。乐观读的标准写法是“先取 stamp → 再读字段 → 再 validate”,屏障放在 validate 开头,是为了保证在比较 state 之前,此前那些对共享字段的读取已经完成、不会被上提。
需要注意的是 validate 比较的只是写标志位 + 高位版本区域,单纯悲观读增减不会导致校验失败。
传统读写锁的读操作需要 CAS 修改 state,而乐观读只读取版本号、不做任何修改,读线程之间零竞争。这正是 StampedLock 性能优势的核心来源。
悲观读 readLock
public long readLock() {
// Unsafe 的 opaque 内存读取,**不具备 volatile 的读屏障**,只保证原子读取,不阻止重排;适合这里一次乐观试探
long s = U.getLongOpaque(this, STATE) & RSAFE, nextState;
// 悲观读计数 + 1
if (casState(s, nextState = s + RUNIT))
return nextState;
else
return acquireRead(false, false, 0L);
}
悲观读 就是 先做一次快速乐观尝试:如果当前锁状态安全(无写锁、读计数离上限还很远),直接 CAS 给读计数 + 1,成功就返回新 state 作为 stamp;失败则进入完整阻塞排队逻辑 acquireRead。
为什么用 getLongOpaque 而不是 volatile 读?
这里只是一次试探性快照: 就算读到过期 state,CAS 本身会做正确性校验;opaque 省去 volatile 读的内存屏障开销,最大化快速路径性能。
下面看一下 悲观读 的解锁。
public void unlockRead(long stamp) {
long s, m;
// `stamp & RBITS` 就是 拿到获取锁那一刻的读计数值。
if ((stamp & RBITS) != 0L) {
// 当前 state 的版本戳 和 解锁传入 stamp 的版本戳必须一致。
// 版本戳一旦变了,说明中间发生过写锁获取 + 释放,这个 stamp 已经失效,不能解锁。
while (((s = state) & SBITS) == (stamp & SBITS) &&
((m = s & RBITS) != 0L)) { // 当前全局读计数不能是 0。读计数已经是 0,没有读锁可以释放,直接退出循环抛异常。
// 当前读计数<126,属于 正常区间,没有读计数溢出风险
if (m < RFULL) {
// CAS 尝试把全局读计数减 1
if (casState(s, s - RUNIT)) {
// `m == RUNIT` 等价于 `m == 1`:本次解锁前,全局读计数等于 1。
if (m == RUNIT)
signalNext(head);
return;
}
}
// 也就是读计数等于 126/127,读计数处于高位临界区,不能直接简单 CAS 减 1,交给`tryDecReaderOverflow`处理溢出边界的复杂逻辑。
else if (tryDecReaderOverflow(s) != 0L)
return;
}
}
throw new IllegalMonitorStateException();
}
需要注意的是,解锁只校验版本戳,不校验线程:没有任何地方校验调用 unlockRead 的线程是不是当初拿 readLock 的线程。任意线程只要拿到合法有效的 stamp,都可以调用 unlockRead 去减掉全局读计数。这是 StampedLock 一个很危险的特性。
唤醒动作只在 m == RUNIT 时触发,也就是从 1→0 的时候,意味着中间那些读线程释放时不会去惊动队列。signalNext(head):唤醒 CLH 队列 head 后继节点,如果是读节点,会连带唤醒挂载在它cowait链表上所有等待读线程;如果是写节点,只唤醒写线程。
写锁 writeLock
先用 Opaque 读拿一次 state 快照,清除低 8 位,尝试 CAS 把 bit7 置 1(加写锁); CAS 成功代表拿到写锁,加写屏障,返回新 state 作为 stamp; CAS 失败说明当前有读锁 / 写锁,进入acquireWrite阻塞排队。
/**
* 获取写锁(不可重入)。
* 快路径:无条件尝试一次 CAS,用 CAS 本身来确认之前那次弱读是否仍然有效。
*/
public long writeLock() {
// opaque 读:不保证有序性,仅作为 CAS 的候选期望值;
// & ~ABITS 抹掉低 8 位(读计数 + 写标志),只保留高位版本区域。
// 若 CAS 成功,说明读取到写入之间低 8 位未被他人改动。
long s = U.getLongOpaque(this, STATE) & ~ABITS, nextState;
if (casState(s, nextState = s | WBIT)) {
// store-store 屏障:保证后续对共享数据的写不会被重排到“拿到写锁”之前,
// 避免其他线程通过乐观读 + validate 观察到不一致的中间状态。
U.storeStoreFence();
return nextState;
}
// 快路径失败,进入自旋 + 入队的完整流程
// 参数含义依次为:可中断、超时控制、截止时间
return acquireWrite(false, false, 0L);
}
总的来说,加写锁,就是 CAS,把 bit7 置 1,标记写锁已持有;高位版本戳保持不变。版本戳只在 unlockWrite 释放写锁时才 + 1。
/**
* 释放写锁。
* 校验两条:stamp 必须与当前 state 完全一致;且 stamp 本身必须带写锁标志位。
*/
public void unlockWrite(long stamp) {
if (state != stamp || (stamp & WBIT) == 0L)
throw new IllegalMonitorStateException();
releaseWrite(stamp);
}
/**
* 实际释放动作:更新 state 后唤醒等待队列中的下一个节点。
* 注意这里直接赋值给 state 字段,而非 CAS——因为写锁是独占的,
* 持有者唯一,不存在并发修改。
*/
private long releaseWrite(long s) {
long nextState = state = unlockWriteState(s);
signalNext(head);
return nextState;
}
/**
* 计算写锁释放后的新 state。
* 这里用的是「加」而不是「减」:释放时再加一个 WBIT,
* 使每次写锁的获取/释放都会在高位留下痕迹,
* 既缓解 CAS 的 ABA 问题,也让 validate 能感知到期间发生过写操作。
* 若加法导致整体归零(理论上仅在全位翻转时出现),则回退到 ORIGIN,
* 因为 0 在 StampedLock 语义里是「操作失败」的哨兵值,不能作为正常状态。
*/
private static long unlockWriteState(long s) {
return ((s += WBIT) == 0L) ? ORIGIN : s;
}
升级与降级 Convert
写锁降级到读锁:
/**
* 尝试把当前持有的锁「降级/转换」为悲观读锁。
* 语义:若 stamp 表示持有写锁,则释放写锁并获得读锁;
* 若已是读锁,直接返回原 stamp;
* 若是乐观读,仅在立即可用时才获得读锁并返回读戳记;
* 其他情况一律返回 0。
*/
public long tryConvertToReadLock(long stamp) {
long a, s, nextState;
// 前置校验:屏蔽读计数后比较版本区域,确认从拿到这个 stamp 至今
// 没有发生过写锁的获取/释放,否则整个转换无意义,直接退出。
while (((s = state) & SBITS) == (stamp & SBITS)) {
a = stamp & ABITS; // 取出 stamp 的低 8 位(读计数 + 写标志)
if (a >= WBIT) {
// —— 分支一:stamp 是写锁
// 写锁独占,要求 state 与 stamp 完全一致,否则说明状态已被改动
if (s != stamp)
break;
// 先走 unlockWriteState 释放写锁(加 WBIT 递增版本),
// 再 + RUNIT 直接挂上读计数,一步完成「释放写 + 获取读」。
// 注意这里是普通赋值而非 CAS——写锁持有者唯一,无并发修改。
nextState = state = unlockWriteState(s) + RUNIT;
// 写锁释放后需要唤醒等待队列中的下一个节点
signalNext(head);
return nextState;
} else if (a == 0L) {
// —— 分支二:stamp 是乐观读(低 8 位全 0)
// 未接近溢出:直接 CAS 给读计数加 1
if ((s & ABITS) < RFULL) {
if (casState(s, nextState = s + RUNIT))
return nextState;
} else if ((nextState = tryIncReaderOverflow(s)) != 0L) {
// 已接近溢出:走溢出计数兜底,返回非 0 表示转换成功
return nextState;
}
// CAS 失败则回到 while 重新取 state 再判断
} else {
// —— 分支三:stamp 本身已经是读锁
// 若当前 state 的低 8 位已归零,说明读计数被别的路径清掉了,
// 这个 stamp 已不可信,退出。
if ((s & ABITS) == 0L)
break;
// 否则无需任何修改,原样返回
return stamp;
}
}
// 版本不匹配或各分支均不满足,转换失败
return 0L;
}
第一个分支就是从独占写锁,变成共享悲观读锁。
- 清除 bit7 WBIT 写标记,版本戳 +1;
- 低 7 位读计数 +1
- 直接赋值给
state,不需要 CAS
当前线程持有写锁,写锁是排他锁,全局没有任何其他线程能修改 state。所以可以直接赋值,不存在并发竞争。
如果本身就是 悲观读锁 ,校验当前 state 还有效(ABITS≠0),直接返回原来的 stamp,啥也不改。
降级很简单,ReentrantReadWriteLock 的写锁也可以降级,那升级呢?
/**
* 尝试把当前 stamp 表示的锁「升级」为写锁。
* 成功返回新的写戳记;任何不满足条件的情况一律返回 0,调用方需自行兜底。
*/
public long tryConvertToWriteLock(long stamp) {
// a:stamp 侧的低 8 位(读计数 + 写标志),用于判断「我手里是什么锁」
// m:state 侧的低 8 位,用于判断「锁当前实际处于什么状态」
long a = stamp & ABITS, m, s, nextState;
// 前置校验:屏蔽读计数后比较版本区域。
// 若期间发生过写锁的获取/释放,SBITS 区域已变化,整个升级无意义,直接退出。
while (((s = state) & SBITS) == (stamp & SBITS)) {
if ((m = s & ABITS) == 0L) {
// —— 分支一:state 低 8 位为 0,说明当前无锁(乐观读不修改 state,也表现为 0)
// 此时若 a 非 0,说明我手里其实持有读锁或写锁,与 state 现状矛盾,属于异常,退出。
if (a != 0L)
break;
// 无锁场景:CAS 置写标志位。成功后加 store-store 屏障,
// 保证后续对共享数据的写不会被重排到「拿到写锁」之前。
if (casState(s, nextState = s | WBIT)) {
U.storeStoreFence();
return nextState;
}
// CAS 失败说明有线程抢先改了 state,回到 while 重新取值判断
} else if (m == WBIT) {
// —— 分支二:state 当前是写锁
// 只有当我手里的 stamp 也是写锁时才合法,否则说明状态已被他人变更。
if (a != m)
break;
// 已经是写锁,无需任何修改,原样返回(注意:这不代表可重入,
// 只是「你本来就持有」的幂等返回)
return stamp;
} else if (m == RUNIT && a != 0L) {
// —— 分支三:state 里只有 1 个读计数,且我手里确实持有读锁
// 这是「唯一读者」的判定条件:减掉自己的读计数同时置上写标志,一步完成升级。
// 若还有其他读者,m 会大于 RUNIT,直接落到最后的 else break。
if (casState(s, nextState = s - RUNIT + WBIT))
return nextState;
} else {
// —— 其他情况:存在多个读者、或读写状态混合等,均不可安全升级
break;
}
}
// 版本不匹配或各分支均不满足,升级失败
return 0L;
}
前文讲过,因为读锁是共享的。假如线程 A 和线程 B 同时持有读锁,同时想要升级到写锁,但线程 A 想要升级到写锁需要等待 线程 B 的读锁释放,而线程 B 想要升级到写锁又需要等待线程 A 的读锁释放,循环等待,死锁了。
这也是为啥 ReentrantReadWriteLock 明确禁止从读锁升级为写锁
但 StampedLock 没有一刀切禁止读转写,而是加了严格前置条件:只有全局读计数恰好等于 1(当前线程就是唯一读者,不存在其他任何读线程),才允许原子 CAS 升级。此时没有其他读者,自然不会出现 A、B 互相等待的死锁场景。
使用场景
StampedLock 适合读多写少、读操作简短、希望降低读开销的场景,依靠乐观读提升并发吞吐量,支持写锁原子降级读锁。
但它牺牲了可重入、Condition 和可中断能力,适合"读极多、写极少、操作极短"的特定场景,不是 ReentrantReadWriteLock 的通用替代品。
-
不可重入:StampedLock 本身不可重入。如果在写锁持有期间再次尝试获取写锁会死锁。需要重入的场景应自行在外层处理。
-
不支持 Condition:没有
newCondition()方法,无法实现等待/通知的精细化控制。 -
不可中断:
readLock()和writeLock()都是阻塞等待,不能被Thread.interrupt()中断。
总结
StampedLock 的核心价值是乐观读——用版本号戳代替传统读锁,在写极少的场景下实现接近无锁的读取性能。
| 维度 | ReentrantReadWriteLock | StampedLock |
|---|---|---|
| 乐观读 | 不支持 | 支持(tryOptimisticRead) |
| 可重入 | 读写锁各自可重入 | 不可重入 |
| Condition | 支持多个 Condition | 不支持 |
| 可中断 | lockInterruptibly 支持 | 不支持 |
| 锁升级 | 不支持 | 支持(convertToWriteLock) |
| 锁降级 | 支持 | 不支持 |
| 基于 AQS | 是 | 否(自研位操作) |
| 适用场景 | 读多写少、需 Condition | 极高读占比、读操作短平快 |
附:UML 类图
@startuml
abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer{}
class ReentrantReadWriteLock$Sync extends AbstractQueuedSynchronizer{}
class ReentrantReadWriteLock$FairSync extends ReentrantReadWriteLock$Sync{}
class ReentrantReadWriteLock$NonfairSync extends ReentrantReadWriteLock$Sync{}
class ReentrantLock$Sync extends AbstractQueuedSynchronizer{}
class ReentrantLock$FairSync extends ReentrantLock$Sync{}
class ReentrantLock$NonfairSync extends ReentrantLock$Sync{}
class Semaphore$Sync extends AbstractQueuedSynchronizer {}
class Semaphore$FairSync extends Semaphore$Sync{}
class Semaphore$NonfairSync extends Semaphore$Sync{}
class CountDownLatch$Sync extends AbstractQueuedSynchronizer {}
class LimitLatch$Sync extends AbstractQueuedSynchronizer {}
@enduml