StampedLock:比读写锁更快的锁🔒
StampedLock 支持的三种锁模式
Java 在 1.8 这个版本里,提供了一种叫 StampedLock 的锁,它的性能就
比读写锁还要好。
StampedLock 支持三种模式,分别是:写锁、悲观读锁和乐观读。其中,写锁、悲观读锁的语义和 ReadWriteLock的写锁、读锁的语义非常类似,允许多个线程同时获取悲观读锁,但是只允许一个线程获取写锁,写锁和悲观读锁是互斥的。不同的是:StampedLock 里的写锁和悲观读锁加锁成功之后,都会返回一个 stamp;然后解锁的时候,需要传入这个 stamp。
final StampedLock sl = new StampedLock();
// 获取 / 释放悲观读锁示意代码
long stamp = sl.readLock();
try {
// 省略业务相关代码
} finally {
sl.unlockRead(stamp);
}
// 获取 / 释放写锁示意代码
long stamp = sl.writeLock();
try {
// 省略业务相关代码
} finally {
sl.unlockWrite(stamp);
}
StampedLock 的性能之所以比 ReadWriteLock 还要好,其关键是 StampedLock 支持乐观读的方式。ReadWriteLock 支持多个线程同时读,但是当多个线程同时读的时候,所有的写操作会被阻塞;而 StampedLock 提供的乐观读,是允许一个线程获取写锁的,也就是说不是所有的写操作都被阻塞。乐观读这个操作是无锁的,所以相比较 ReadWriteLock 的读锁,乐观读的性能更好一些。
来看一个例子:
class Point {
private int x, y;
final StampedLock sl = new StampedLock();
// 计算到原点的距离
int distanceFromOrigin() {
// 乐观读
long stamp = sl.tryOptimisticRead();
// 读入局部变量,
// 读的过程数据可能被修改
int curX = x, curY = y;
// 判断执行读操作期间,
// 是否存在写操作,如果存在,
// 则 sl.validate 返回 false
if (!sl.validate(stamp)){
// 升级为悲观读锁
stamp = sl.readLock();
try {
curX = x;
curY = y;
} finally {
// 释放悲观读锁
sl.unlockRead(stamp);
}
}
return Math.sqrt(curX * curX + curY * curY);
}
}
在上面这个代码示例中,如果执行乐观读操作的期间,存在写操作,会把乐观读升级为悲观读锁。这个做法挺合理的,否则你就需要在一个循环里反复执行乐观读,直到执行乐观读操作的期间没有写操作(只有这样才能保证 x 和 y 的正确性和一致性),而循环读会浪费大量的 CPU。升级为悲观读锁,代码简练且不易出错,实际使用中也推荐这样。
深入了解乐观锁
如果你曾经用过数据库的乐观锁,可能会发现 StampedLock 的乐观读和数据库的乐观锁有异曲同工之妙。
类比数据库乐观锁的一个常见实用场景:在 ERP 的生产模块里,会有多个人通过 ERP 系统提供的 UI 同时修改同一条生产订单,那如何保证生产订单数据是并发安全的呢?通常采用的方案就是乐观锁。
数据库乐观锁的实现很简单,在生产订单的表 product_doc 里增加了一个数值型版本号字段version,每次更新 product_doc 这个表的时候,都将 version 字段加 1。生产订单的 UI在展示的时候,需要查询数据库,此时将这个 version 字段和其他业务字段一起返回给生产订单 UI。
你会发现数据库里的乐观锁,查询的时候需要把 version 字段查出来,更新的时候要利用version 字段做验证。这个 version 字段就类似于 StampedLock 里面的 stamp。
使用StampedLock 需要注意什么
对于读多写少的场景 StampedLock 性能很好,简单的应用场景基本上可以替代ReadWriteLock,但是StampedLock 的功能仅仅是 ReadWriteLock 的子集。使用时需要注意以下几点:
- StampedLock 不支持锁重入;
- StampedLock 的悲观读锁、写锁都不支持Condition条件变量;
- 如果线程阻塞在 StampedLock 的 readLock() 或者writeLock() 上时,此时调用该阻塞线程的 interrupt() 方法,会导致 CPU 飙升。
⚠️:使用 StampedLock 一定不要调用中断操作,如果需要支持中断功能,一定使用可中断的悲观读锁 readLockInterruptibly() 和写锁writeLockInterruptibly()。
总结:
觉得有用的客官可以点赞、关注下!感谢支持🙏谢谢!