上一篇用ReadWriteLock把共享读取与独占更新分开了。对于一些字段少、读取短、更新较少的数据,还可以尝试另一种读取路径:先把数据复制到局部变量,检查读取期间有没有写锁介入,再决定这份结果能否使用。
StampedLock提供了这样的乐观读能力。本文基于Java 21,用一对必须保持关系的坐标说明读取、校验与回退;示例关注正确性,不提供未经测量的性能结论。
一、三种模式,先分清谁持有锁
| 模式 | 主要方法 | 使用含义 |
|---|---|---|
| 写锁 | writeLock、unlockWrite | 独占更新共享数据 |
| 读锁 | readLock、unlockRead | 在保护下读取,阻止写锁进入 |
| 乐观读 | tryOptimisticRead、validate | 取得校验用的stamp,读取后检查有效性 |
获取读锁或写锁后,返回的stamp需要用于对应的释放操作。乐观读没有取得普通读锁,不能调用unlockRead释放它。
tryOptimisticRead 在当前持有写锁时返回0;validate 检查取得stamp后是否有写锁被获取,stamp为0时校验失败。普通读取路径和写入路径都必须使用同一个StampedLock。契约见Java 21 StampedLock文档。
二、读取结果先放在手里,校验通过再使用
假设两个字段x和y始终满足y等于x的两倍。写线程修改这两个字段时,读线程可能恰好看到一新一旧的组合。因此,乐观读取期间得到的数据暂时只能当作候选值。

图中的凭据对应stamp;"校验"决定候选数据能否交给调用者。发生更新后,旧凭据失效,读取路径转入读锁保护,并重新复制全部需要的字段。
常见流程是:取得stamp→复制字段→validate→成功则使用局部副本,失败则持读锁重新读取。先校验再读取共享字段会留下新的竞争空档,顺序不能颠倒。
三、完整示例:把相关字段一起复制
保存为StampedDemo.java,使用JDK21运行:
java
import java.util.concurrent.*;
import java.util.concurrent.locks.StampedLock;
public class StampedDemo {
record Snapshot(int x, int y) {}
static class Point {
private int x = 1;
private int y = 2;
private final StampedLock lock = new StampedLock();
void move(int dx, int dy) {
long stamp = lock.writeLock();
try {
x += dx;
y += dy;
} finally {
lock.unlockWrite(stamp);
}
}
Snapshot snapshot() {
long stamp = lock.tryOptimisticRead();
int copyX = x;
int copyY = y;
if (!lock.validate(stamp)) {
stamp = lock.readLock();
try {
copyX = x;
copyY = y;
} finally {
lock.unlockRead(stamp);
}
}
return new Snapshot(copyX, copyY);
}
}
public static void main(String[] args) throws Exception {
Point point = new Point();
System.out.println("initial: " + point.snapshot());
// 单独演示:获取凭据后发生写入,旧凭据失效。
long oldStamp = point.lock.tryOptimisticRead();
point.move(2, 4);
System.out.println("old stamp valid: " + point.lock.validate(oldStamp));
System.out.println("after move: " + point.snapshot());
try (ExecutorService pool = Executors.newFixedThreadPool(2)) {
Future<?> writer = pool.submit(() -> {
for (int i = 0; i < 1000; i++) {
point.move(1, 2);
}
});
Future<Integer> reader = pool.submit(() -> {
for (int i = 0; i < 1000; i++) {
Snapshot copy = point.snapshot();
if (copy.y() != copy.x() * 2) {
throw new AssertionError("inconsistent snapshot: " + copy);
}
}
return 1000;
});
writer.get();
System.out.println("consistent snapshots: " + reader.get());
}
}
}
shell
javac StampedDemo.java
java StampedDemo
输出:
text
initial: Snapshot[x=1, y=2]
old stamp valid: false
after move: Snapshot[x=3, y=6]
consistent snapshots: 1000
这个示例已在本机JDK21编译运行。前半段固定展示旧stamp失效,后半段检查1000次返回快照的字段关系。线程调度可能不同,输出没有统计乐观读成功率;一次运行也不能代替完整的并发验证或基准测试。
校验成功后使用的是copyX与copyY。这时其他线程仍可能继续写入,但不会改变已经复制出来的int值。Snapshot是包含两个基本类型字段的record,交给调用者后无需继续持锁。
四、为什么HashMap不能照搬这段读取方式
上面的乐观读段只有两个简单字段读取。换成普通HashMap的遍历、查找或复制,需要考虑集合在并发修改过程中的内部状态;调用过程中已经发生的异常或不安全行为,无法靠最后一次validate补救。
乐观读段应保持短小、只读,并能容忍候选字段暂时不一致。读取对象引用后继续调用方法、访问数组元素或做依赖多字段的计算时,应单独分析安全性。难以证明时,直接持读锁读取,或返回经过设计的不可变数据。
尤其不要在校验前基于候选数据发送请求、写文件或更新计数器。即使之后校验失败,这些副作用也已经发生。校验失败时,应丢弃候选值并重新读取。
五、转换、重入与实际取舍
StampedLock不可重入。持有写锁的方法再次调用会获取同一个写锁的方法,可能把自己阻塞住。锁内尽量只做明确的小操作,避免调用未知回调。它也没有直接提供Condition;需要条件等待时应重新选择协调结构。
tryConvertToWriteLock 可以尝试把已有模式转换为写锁,失败返回0。转换失败后,原来持有的读锁仍需正确释放;重新竞争写锁后,再检查业务前提。不要用失败返回值覆盖原stamp后忘记释放原锁。相关使用约定也可对照OpenJDK21的StampedLock源码与注释。
| 选择时关心的问题 | 可以考虑的方向 |
|---|---|
| 锁内有可重入调用或需要Condition | ReentrantLock或ReentrantReadWriteLock |
| 数据简单,读取短,能够安全复制后校验 | 分析StampedLock的乐观读 |
| 数据结构复杂,校验前访问难以保证安全 | 先用普通读锁或不可变快照 |
| 更新频繁,回退较多 | 测量竞争与回退成本,再决定 |
"读操作多"只是选型线索。真正影响收益的还有读取长度、写入频率、回退概率和数据表示。把实现复杂度与维护成本一起纳入比较,比只看API名称更有帮助。
六、🧠 思维导图
#mermaid-svg-gFeq3VI6Vsmoppnc{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-gFeq3VI6Vsmoppnc .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-gFeq3VI6Vsmoppnc .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-gFeq3VI6Vsmoppnc .error-icon{fill:#552222;}#mermaid-svg-gFeq3VI6Vsmoppnc .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-gFeq3VI6Vsmoppnc .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-gFeq3VI6Vsmoppnc .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-gFeq3VI6Vsmoppnc .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-gFeq3VI6Vsmoppnc .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-gFeq3VI6Vsmoppnc .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-gFeq3VI6Vsmoppnc .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-gFeq3VI6Vsmoppnc .marker{fill:#333333;stroke:#333333;}#mermaid-svg-gFeq3VI6Vsmoppnc .marker.cross{stroke:#333333;}#mermaid-svg-gFeq3VI6Vsmoppnc svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-gFeq3VI6Vsmoppnc p{margin:0;}#mermaid-svg-gFeq3VI6Vsmoppnc .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-gFeq3VI6Vsmoppnc .cluster-label text{fill:#333;}#mermaid-svg-gFeq3VI6Vsmoppnc .cluster-label span{color:#333;}#mermaid-svg-gFeq3VI6Vsmoppnc .cluster-label span p{background-color:transparent;}#mermaid-svg-gFeq3VI6Vsmoppnc .label text,#mermaid-svg-gFeq3VI6Vsmoppnc span{fill:#333;color:#333;}#mermaid-svg-gFeq3VI6Vsmoppnc .node rect,#mermaid-svg-gFeq3VI6Vsmoppnc .node circle,#mermaid-svg-gFeq3VI6Vsmoppnc .node ellipse,#mermaid-svg-gFeq3VI6Vsmoppnc .node polygon,#mermaid-svg-gFeq3VI6Vsmoppnc .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-gFeq3VI6Vsmoppnc .rough-node .label text,#mermaid-svg-gFeq3VI6Vsmoppnc .node .label text,#mermaid-svg-gFeq3VI6Vsmoppnc .image-shape .label,#mermaid-svg-gFeq3VI6Vsmoppnc .icon-shape .label{text-anchor:middle;}#mermaid-svg-gFeq3VI6Vsmoppnc .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-gFeq3VI6Vsmoppnc .rough-node .label,#mermaid-svg-gFeq3VI6Vsmoppnc .node .label,#mermaid-svg-gFeq3VI6Vsmoppnc .image-shape .label,#mermaid-svg-gFeq3VI6Vsmoppnc .icon-shape .label{text-align:center;}#mermaid-svg-gFeq3VI6Vsmoppnc .node.clickable{cursor:pointer;}#mermaid-svg-gFeq3VI6Vsmoppnc .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-gFeq3VI6Vsmoppnc .arrowheadPath{fill:#333333;}#mermaid-svg-gFeq3VI6Vsmoppnc .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-gFeq3VI6Vsmoppnc .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-gFeq3VI6Vsmoppnc .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-gFeq3VI6Vsmoppnc .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-gFeq3VI6Vsmoppnc .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-gFeq3VI6Vsmoppnc .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-gFeq3VI6Vsmoppnc .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-gFeq3VI6Vsmoppnc .cluster text{fill:#333;}#mermaid-svg-gFeq3VI6Vsmoppnc .cluster span{color:#333;}#mermaid-svg-gFeq3VI6Vsmoppnc div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-gFeq3VI6Vsmoppnc .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-gFeq3VI6Vsmoppnc rect.text{fill:none;stroke-width:0;}#mermaid-svg-gFeq3VI6Vsmoppnc .icon-shape,#mermaid-svg-gFeq3VI6Vsmoppnc .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-gFeq3VI6Vsmoppnc .icon-shape p,#mermaid-svg-gFeq3VI6Vsmoppnc .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-gFeq3VI6Vsmoppnc .icon-shape .label rect,#mermaid-svg-gFeq3VI6Vsmoppnc .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-gFeq3VI6Vsmoppnc .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-gFeq3VI6Vsmoppnc .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-gFeq3VI6Vsmoppnc :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} StampedLock
三种模式
读锁与写锁
乐观读不持读锁
读取协议
先复制后校验
失败持读锁重读
数据边界
候选值不能产生副作用
谨慎访问可变结构
使用取舍
不可重入
转换失败与性能测量
七、总结
总结要点
乐观读与stamp提供一条先复制、再检查的读取路径。它不阻止写入,候选数据只有通过校验后才能使用。
失败回退与数据封装决定这条路径是否完整。校验失败后持读锁重读全部相关字段,调用者使用局部快照,避免绕开锁协议。
数据表示与使用成本要一起考虑。简单字段适合分析乐观读,复杂可变结构应谨慎;不可重入、转换失败和回退成本都影响实现选择。
下一篇继续讨论AtomicInteger与CAS,看看单个共享值如何完成原子更新,以及它能覆盖哪些业务约束。
👉 如果你觉得这篇文章对你有所帮助,欢迎点赞、收藏、分享!😊