在多线程并发访问共享资源的战场上,开发者长期依赖synchronized和ReadWriteLock作为守护神。然而,在极端“读多写少”的场景下,ReadWriteLock的悲观锁机制仍可能导致写线程饥饿和吞吐量下降。Java 8引入的StampedLock以其卓越的性能和灵活的乐观读机制,为我们提供了新的选择。
一、StampedLock核心思想:乐观与悲观的权衡
StampedLock并非一种纯粹的悲观锁或乐观锁,而是巧妙地将两者结合。它的核心是一个用于表示锁状态的long类型的戳记(Stamp)。所有获取锁的方法都会返回一个Stamp,解锁时则必须传入这个Stamp作为凭证。
它提供了三种访问模式:
写锁(Writing):独占锁,与ReadWriteLock的写锁类似。一旦获取,其他任何读写线程都必须阻塞等待。方法返回一个Stamp。
long stamp = lock.writeLock();
try {
// 进行写操作
} finally {
lock.unlockWrite(stamp); // 解锁必须使用对应的Stamp
}
悲观读锁(Reading):共享锁,与ReadWriteLock的读锁类似。获取读锁时,允许多个读线程同时进入,但会阻塞所有的写线程。方法返回一个Stamp。
long stamp = lock.readLock();
try {
// 进行读操作
} finally {
lock.unlockRead(stamp);
}
乐观读(Optimistic Reading):这是StampedLock的精髓所在。它允许一个读线程在不获取锁的情况下进行读操作。它不会阻塞任何线程,包括写线程。
long stamp = lock.tryOptimisticRead(); // 1. 尝试获取一个乐观读戳记
// 2. 将共享数据读取到局部变量中(例如:data = sharedData;)
if (!lock.validate(stamp)) { // 3. 校验:在读的过程中是否有写操作发生?
stamp = lock.readLock(); // 3.1. 校验失败,说明有写操作,升级为悲观读锁
try {
// 3.2. 在悲观读锁的保护下,重新读取数据
} finally {
lock.unlockRead(stamp);
}
}
// 4. 使用局部变量中的数据执行业务逻辑
关键在于validate(stamp)方法,它在读取数据后检查自获取Stamp以来,是否有写锁被获取过。如果没有,说明这次读操作是安全的;如果有,则必须“降级”为悲观读锁重新读取数据,以确保数据一致性。
二、深度分析与最佳实践
- 性能优势:乐观读避免了读线程与读线程之间的竞争,只有在极少数发生读写冲突时才需要真正获取锁,这极大地提升了读操作的吞吐量。
- 非可重入:
StampedLock是不可重入的。如果一个线程已经持有了写锁,再次尝试获取写锁会导致死锁。这与ReentrantReadWriteLock有本质区别,使用时必须格外小心。 - 条件队列:
StampedLock没有内置的条件变量(Condition)。如果需要await()/signal()功能,必须通过其他方式实现,这在一定程度上限制了其使用场景。 - Stamp管理:Stamp是解锁的唯一凭证,必须妥善保管并在
finally块中解锁,否则可能导致死锁。
三、实战示例:一个线程安全的坐标点类
下面是一个使用StampedLock来保护一个(x, y)坐标点的完整示例,演示了写锁、悲观读锁和乐观读三种模式的使用。
import java.util.concurrent.locks.StampedLock;
public class Point {
private double x, y;
private final StampedLock sl = new StampedLock();
// 写操作:独占锁
void move(double deltaX, double deltaY) {
long stamp = sl.writeLock(); // 获取写锁
try {
x += deltaX;
y += deltaY;
} finally {
sl.unlockWrite(stamp); // 释放写锁
}
}
// 读操作:悲观读锁(传统方式,安全但性能一般)
double distanceFromOrigin() {
long stamp = sl.readLock();
try {
return Math.sqrt(x * x + y * y);
} finally {
sl.unlockRead(stamp);
}
}
// 读操作:乐观读(高性能方式)
double distanceFromOriginOptimistic() {
long stamp = sl.tryOptimisticRead(); // 1. 尝试乐观读
double currentX = x, currentY = y; // 2. 读取数据到局部变量
// 3. 检查在读的过程中是否有写操作发生
if (!sl.validate(stamp)) {
// 3.1 校验失败,升级为悲观读锁
stamp = sl.readLock();
try {
currentX = x; // 重新读取x
currentY = y; // 重新读取y
} finally {
sl.unlockRead(stamp);
}
}
// 3.2 校验成功,直接使用最初读取的currentX和currentY
return Math.sqrt(currentX * currentX + currentY * currentY);
}
}
结论
StampedLock是一把为追求极致性能的并发专家准备的“神兵利器”。它通过引入乐观读模式,在保证线程安全的前提下,巧妙地绕过了读锁竞争,完美契合了“读多写少”的应用场景。然而,其不可重入的特性和缺乏条件变量的支持也带来了更高的编程复杂度。
选择与否,取决于具体的应用场景:如果你的代码模式是典型的ReadWriteLock适用场景,且对性能有极致要求,并能妥善处理其复杂性,那么StampedLock将是你的不二之选。否则,成熟的ReentrantReadWriteLock或synchronized或许是更稳妥、更简单的方案。
271

被折叠的 条评论
为什么被折叠?



