什么是StampedLock?
StampedLock是在Java 8中引入的一种高性能读写锁 ,它通过引入stamp,提供了比ReentrantReadWriteLock更加灵活、更细粒度的并发控制机制,特别适合读多写少的场景,并且它没有使用AQS。
StampedLock是一种基于stamp的锁机制,支持三种访问模式:
| 模式 | 说明 |
|---|---|
| 写锁(WriteLock) | 独占锁,类似 ReentrantReadWriteLock.WriteLock |
| 悲观读锁(ReadLock) | 共享锁,类似 ReentrantReadWriteLock.ReadLock |
| 乐观读(Optimistic Read) | 无锁读取,不阻塞写线程 |
它在每次加锁都会返回一个long类型的stamp,解锁的时候必须使用stamp。
为什么要使用StampedLock
对于传统的读写锁ReentrantReadWriteLock,它存在两个问题:
- 读锁饥饿写锁 :如果读线程非常多,那么会导致写线程可能长时间拿不到锁 2.读锁也是重量级锁 :即便是读,也需要
CAS+队列排队,仍然有开销
为了解决这些问题,StampedLock的核心思想是:能用无锁就不用加锁。
尤其是引入了乐观锁,在读多写极少的情况下性能极佳。
StampedLock三种锁模式
写锁writeLock
这是一个独占锁,同一时刻只有一个线程可以获取,其他请求写锁和读锁的线程必须等待,类似于ReentrantReadWriteLock的写锁,但是一个重要的不同之处是:StampedLock的写锁是不可重入锁。
请求该锁成功后会返回一个stamp变量用来表示该锁的版本 ,当释放该锁时需要调用unlockWrite方法并传递获取锁时的stamp参数。并且它提供了非阻塞的tryWriteLock方法。
悲观读锁readLock
这是一个共享锁,在没有线程持有写锁的时候,多个线程可以同时持有读锁,如果已经有线程持有写锁,则其他线程请求获取该读锁会被阻塞,类似于ReentrantReadWriteLock的读锁,但是一个重要的不同之处是:StampedLock的读锁是不可重入锁。
请求该锁成功后会返回一个stamp变量用来表示该锁的版本 ,当释放该锁时需要调用unlockRead方法并传递stamp参数。并且它提供了非阻塞的tryReadLock方法。
乐观读锁tryOptimisticRead
相对于悲观读锁,在操作数据前并没有通过CAS设置锁的状态,仅仅通过位运算来测试。
如果当前没有线程持有写锁,则简单地返回一个非0的stamp版本信息,获取了stamp后在具体操作数据前还需要调用validate方法验证该stamp是否已经不可用。如果在这期间,有线程获取了写锁,那么validate会返回0。
乐观读锁不需要设置锁的状态,因此也不需要释放锁,效率会高很多,但也因此在保证数据一致性上需要复制一份要操作的变量到方法栈,并且操作数据的时候,可能其他写线程已经修改了数据,而我们操作的是方法栈中的数据,也就是原本数据的一个快照。
三种锁相互转换
StampedLock还支持这三种在一定条件下进行相互转换。tryConvertToWriteLock(long stamp)期望把stamp标示的锁升级为写锁,这个函数会在以下的情况下返回一个有效的stamp代表晋升写锁成功:
- 当前锁已经是写锁
- 当前锁处于读锁,并且没有其他线程持有读锁
- 当前锁是乐观读锁,并且当前写锁可用

StampedLock使用示例
java
class Point {
// 成员变量
private double x, y;
// 锁实例
private final StampedLock sl = new StampedLock();
// 排它锁------写锁(writeLock)
void move(double deltaX, double deltaY) {
long stamp = sl.writeLock();
try {
x += deltaX;
y += deltaY;
} finally {
sl.unlockWrite(stamp);
}
}
// 乐观读锁(tryOptimisticRead)
double distanceFromOrigin() {
// (1) 尝试获取乐观读锁
long stamp = sl.tryOptimisticRead();
// (2) 将全部变量复制到方法体域内
double currentX = x, currentY = y;
// (3) 检查在(1)处获取了读锁戳记后,锁有没有被其他写线程排他性抢占
if (!sl.validate(stamp)) {
// (4) 如果被抢占则获取一个共享读锁(悲观获取)
stamp = sl.readLock();
try {
// (5) 将全部变量复制到方法体域内
currentX = x;
currentY = y;
} finally {
// (6) 释放共享读锁
sl.unlockRead(stamp);
}
}
// (7) 返回计算结果
return Math.sqrt(currentX * currentX + currentY * currentY);
}
// 使用悲观锁获取读锁,并尝试转换为写锁
void moveIfAtOrigin(double newX, double newY) {
// (1) 这里可以使用乐观读锁替换
long stamp = sl.readLock();
try {
// (2) 如果当前点在原点则移动
while (x == 0.0 && y == 0.0) {
// (3) 尝试将获取的读锁升级为写锁
long ws = sl.tryConvertToWriteLock(stamp);
// (4) 升级成功,则更新戳记,并设置坐标值,然后退出循环
if (ws != 0L) {
stamp = ws;
x = newX;
y = newY;
break;
} else {
// (5) 读锁升级写锁失败则释放读锁,显式获取独占写锁,然后循环重试
sl.unlockRead(stamp);
stamp = sl.writeLock();
}
}
} finally {
// (6) 释放锁
sl.unlock(stamp);
}
}
}