读写锁允许多个读者并行,但每次获取读锁仍需维护锁状态。Java 8 的 StampedLock 增加了乐观读:先尝试读取,之后检查期间有没有写操作。
三种访问模式
writeLock 是独占写锁,readLock 是悲观读锁。二者取得的 stamp 用于后续解锁。
tryOptimisticRead() 返回一个用于校验的标记(stamp),并不会真正加上读锁。写线程仍然可以修改数据,所以读取后需要调用 validate(stamp),检查读取期间是否发生了写操作。
import java.util.concurrent.locks.StampedLock;
public class StampedPointDemo {
private final StampedLock lock = new StampedLock();
private double x;
private double y;
public void move(double nextX, double nextY) {
long stamp = lock.writeLock();
try {
x = nextX;
y = nextY;
} finally {
lock.unlockWrite(stamp);
}
}
public double distance() {
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.hypot(currentX, currentY);
}
public static void main(String[] args) throws InterruptedException {
StampedPointDemo point = new StampedPointDemo();
Thread writer = new Thread(() -> point.move(3, 4));
writer.start();
writer.join();
System.out.println(point.distance());
}
}输出 5.0。乐观读取失败后,先取得真正的读锁,再重新读两个字段,不能继续使用前面那组可能不一致的值。
先读到局部变量,再校验
validate 要放在读取之后。先 validate 成功,再从共享对象读取,写者仍可能在这中间进入,前一次校验就不能保护后面的读取。
乐观读取期间得到的字段可能暂时不一致。因此在校验前,不要依据它们做有副作用的操作,也不要随意遍历会并发改变结构的集合。像上面的简单数值快照容易处理,复杂对象图可能直接使用悲观读锁更稳妥。
如果 tryOptimisticRead 返回 0,说明当时有写锁持有者,validate 会失败并进入读锁路径。
锁转换
tryConvertToWriteLock(stamp) 可以尝试转换为写锁,成功返回新的非零 stamp,失败返回 0。只有满足相应条件,例如自己是唯一读者时,读锁才有机会直接转换成功。
转换失败后,原来的读锁并没有因为这次失败自动释放。需要释放原读锁,再获取写锁,并重新检查业务条件。解锁时必须使用当前有效的 stamp和对应模式,不能一直沿用最初的值。
与 ReentrantReadWriteLock 的区别
StampedLock 不支持重入。已经取得写锁,再调用一个也要获取写锁的方法,可能把自己阻塞住。它也没有通常意义上的线程所有权,使用时仍要保存并正确传递 stamp。
它不提供 Condition。需要等待锁时响应中断,可以选择 readLockInterruptibly()、writeLockInterruptibly(),不要把普通 readLock/writeLock 当成可中断获取。
乐观读减少了读者对共享锁状态的更新,但失败时需要重读甚至获取读锁。写入频繁或读取逻辑复杂时,不一定有优势,适合不适合仍然取决于访问模式。