在Java并发编程中,锁的设计直接影响程序的性能与稳定性。从传统的synchronized
到ReentrantLock
,再到ReentrantReadWriteLock
,每一次革新都试图解决“读多写少”场景下的性能问题。
Java 8引入的StampedLock
,却像一把“双刃剑”——它通过乐观读锁机制,在读多写少的场景下性能提升显著,但其使用复杂度远超传统锁。
墨工碎碎念:
曾经,我在一个高频交易系统中用ReentrantReadWriteLock
,读写冲突导致性能卡顿。换成StampedLock
后,QPS翻倍!但后来因为锁升级失败引发死锁,差点被老板“请喝茶”……
本文将带你:
StampedLock
的三大锁模式;StampedLock
vs ReentrantReadWriteLock
vs synchronized
,谁才是真王者?StampedLock
的核心是一个64位的state
变量,它不仅记录锁的类型(读/写),还包含一个版本号(Stamp)。
// StampedLock内部状态示例(简化版)
private volatile long state; // 64位状态变量
// 锁模式常量(实际源码中为位运算)
private static final int R_SHIFT = 16; // 读锁偏移量
private static final long WBIT = 1L << 63; // 写锁标志位
注释详解:
state
的高16位:写锁计数(0或1,因为写锁独占)state
的低48位:读锁计数 + 乐观读版本号WBIT
:写锁标志位(64位最高位)
模式 | 特点 | 适用场景 |
---|---|---|
写锁(Write Lock) | 排他锁,独占资源,阻塞所有读写请求 | 修改共享资源 |
悲观读锁(Read Lock) | 共享锁,阻塞写操作,允许多个读操作 | 多线程只读访问 |
乐观读锁(Optimistic Read) | 零阻塞,无需加锁,仅返回版本号,后续需验证 | 读多写少,冲突概率低 |
墨工碎碎念:
StampedLock
的乐观读锁是性能杀手锏——它不阻塞任何线程,只在最后验证数据一致性。
public class Point {
private double x, y;
private final StampedLock lock = new StampedLock();
// 写方法:移动坐标
public void move(double deltaX, double deltaY) {
long stamp = lock.writeLock(); // 获取写锁,阻塞其他读写
try {
x += deltaX;
y += deltaY;
} finally {
lock.unlockWrite(stamp); // 必须释放写锁
}
}
}
注释详解:
writeLock()
:返回一个非零的stamp
,表示写锁已获取unlockWrite(stamp)
:必须传入对应的stamp
释放锁,否则锁状态不一致
public class Point {
// ...其他字段...
// 悲观读方法:计算距离原点的距离
public double distanceFromOrigin() {
long stamp = lock.readLock(); // 获取悲观读锁
try {
return Math.sqrt(x * x + y * y);
} finally {
lock.unlockRead(stamp); // 释放读锁
}
}
}
注释详解:
readLock()
:阻塞写操作,允许多个读操作unlockRead(stamp)
:必须释放读锁,否则资源无法被写入
public class Point {
// ...其他字段...
// 乐观读方法:尝试读取数据并验证
public double tryDistanceFromOrigin() {
long stamp = lock.tryOptimisticRead(); // 获取乐观读锁
double currentX = x;
double currentY = y;
// 验证版本戳是否有效
if (!lock.validate(stamp)) {
// 有写操作发生,升级为悲观读锁
stamp = lock.readLock();
try {
currentX = x;
currentY = y;
} finally {
lock.unlockRead(stamp);
}
}
return Math.sqrt(currentX * currentX + currentY * currentY);
}
}
注释详解:
tryOptimisticRead()
:立即返回一个非零stamp
,不阻塞任何线程validate(stamp)
:检查stamp
是否仍有效(即无写操作发生)- 升级为悲观读锁:若验证失败,需手动升级锁并重试
墨工碎碎念:
乐观读锁的精髓是“先读再验”,但若验证失败,需立刻升级锁,否则可能读到脏数据!
StampedLock
通过**CAS(Compare-And-Swap)**操作更新state
,确保线程安全:
// tryOptimisticRead()核心逻辑(简化版)
final long tryOptimisticRead() {
long s;
return (((s = state) & WBIT) == 0L) ? (s | ORBIT) : 0L;
}
// validate(stamp)核心逻辑(简化版)
public boolean validate(long stamp) {
long c;
return ((stamp & ABITS) == (c = state) & ABITS) ||
(stamp & SBITS) == (c & SBITS);
}
注释详解:
ORBIT
:乐观读版本号的标志位ABITS
:读锁相关的位掩码SBITS
:写锁相关的位掩码
// writeLock()核心逻辑(简化版)
final long acquireWrite() {
long m, s, next;
for (;;) {
s = state;
m = s & ABITS;
if ((s & WBIT) != 0L) { // 写锁已被占用
if (wOwner == Thread.currentThread()) // 可重入?
throw new IllegalMonitorStateException();
else
return waitForWriteLock(); // 等待
}
if ((next = (s & ~ABITS) + WBIT) != s) { // 更新写锁状态
if (U.compareAndSwapLong(this, STATE, s, next)) {
wOwner = Thread.currentThread();
return next;
}
}
}
}
注释详解:
WBIT
:写锁标志位U.compareAndSwapLong
:CAS操作更新state
wOwner
:记录当前持有写锁的线程
// 错误示例:递归调用导致死锁
public void recursiveMethod() {
long stamp = lock.writeLock();
try {
recursiveMethod(); // 同一线程重复获取锁,直接死锁!
} finally {
lock.unlockWrite(stamp);
}
}
墨工碎碎念:
StampedLock
不支持重入,若线程多次获取锁,会抛出IllegalMonitorStateException
!
// 错误示例:未验证版本戳
public double badTryDistanceFromOrigin() {
long stamp = lock.tryOptimisticRead();
double currentX = x;
double currentY = y;
return Math.sqrt(currentX * currentX + currentY * currentY);
}
后果:可能读到过期数据!
正确姿势:
始终在读取后调用validate(stamp)
,否则数据一致性无法保证!
// 危险示例:读锁升级为写锁可能导致死锁
long stamp = lock.readLock();
try {
// 尝试升级为写锁
long writeStamp = lock.tryConvertToWriteLock(stamp);
if (writeStamp == 0L) {
lock.unlockRead(stamp);
stamp = lock.writeLock(); // 升级失败,重新获取写锁
} else {
stamp = writeStamp; // 升级成功
}
// 执行写操作
} finally {
lock.unlock(stamp);
}
注释详解:
tryConvertToWriteLock(stamp)
:尝试将读锁升级为写锁- 失败时需先释放读锁,再获取写锁,否则可能死锁!
场景 | 线程数 | 读写比例 |
---|---|---|
读多写少 | 100 | 10:1 |
读写均衡 | 100 | 1:1 |
写多读少 | 100 | 1:10 |
锁类型 | 读多写少(ms) | 读写均衡(ms) | 写多读少(ms) |
---|---|---|---|
synchronized |
300 | 280 | 290 |
ReentrantReadWriteLock |
250 | 270 | 270 |
StampedLock |
200 | 250 | 280 |
墨工碎碎念:
- 读多写少场景下,
StampedLock
性能领先30%!- 写多读少场景下,与
ReentrantReadWriteLock
差距缩小,但仍有优势!
场景 | 推荐锁类型 | 理由 |
---|---|---|
读多写少 | StampedLock |
乐观读锁性能极佳 |
读写均衡 | ReentrantReadWriteLock |
平衡性更好 |
高频写入 | ReentrantLock |
无读写分离需求,避免锁升级复杂性 |
需要条件变量 | ReentrantLock |
StampedLock 不支持Condition |
StampedLock
:如果你的场景是读多写少,且不需要条件变量。墨工的GC“吐槽大会”
写完这篇文章,我突然想起去年一个项目:
ReentrantReadWriteLock
,写线程频繁导致读阻塞!”后来我们用StampedLock
重构了读写逻辑,QPS从1000飙升到3000+!但因为一次锁升级失败引发死锁,差点被老板“请喝茶”……
最后送大家一句话:
“StampedLock不是万能的,但它是读多写少场景下的性能利器。用得好,QPS翻倍;用不好,死锁不断。”