上篇文章中介绍了读写锁:
- {% post_link 读书笔记/极客时间/Java并发编程实战/第一部分—基础/17|ReadWriteLock 17 | ReadWriteLock:如何快速实现一个完备的缓存 %}
学完之后知道了读写锁比互斥锁的快的原因在于:读写锁
允许多个线程同时读共享变量,适用于读多写少
的场景。在读多写少的场景中还有一个比读写锁更快的方案,JDK8 新增加了一个叫 StampedLock
的锁,性能比读写锁还要好。
这篇文章介绍 StampedLock
的使用方法
、内部工作原理
、使用时的注意事项
。
StampedLock 支持的三种锁模式
锁类型 | 支持的模式 | 详情 |
---|---|---|
StampedLock | 写锁 、悲观读锁 、乐观读 。 | 相同点: 写锁 :语义类似 ReadWriteLock 写锁悲观读锁 :语义类似 ReadWriteLock 读锁允许多个线程同时获取悲观读锁,但是只允许一个线程获取写锁。 不同: StampedLock 里的 写锁 和悲观读锁 加锁成功后会返回一个 stamp ,解锁的时候需要传入这个对象。 |
ReadWriteLock | 读锁 、写锁 |
StampedLock
相关示例代码:
public class StampedDemo {
public static void main(String[] args) {
final StampedLock sl = new StampedLock();
// 获取/释放 悲观读锁
long readStamp = sl.readLock();
try {
//相关业务代码在这里写
}finally {
sl.unlockRead(readStamp);
}
// 获取/释放 悲观写锁
long writeStamp = sl.writeLock();
try {
//相关业务代码在这里写
}finally {
sl.unlockWrite(writeStamp);
}
}
}
StampedLock
性能之所比 ReadWriteLock
要好,关键就是 StampedLock
支持的**乐观读
方式。ReadWriteLock
支持多个线程同时读
,但是当多个线程同时读时,写操作
会被阻塞
。而 StampedLock
提供的乐观读
允许在读的同时
有一个线程
获取写锁,也就是说不是所有写操作都被阻塞**。
这里作者强调的是 「乐观读
」而不是乐观读锁
,说明这个操作是无锁
操作,所以相比 ReadWriteLock
的读锁,乐观读的性能更好一些。
下面这段代码来自 JDK 官方示例,并略做修改。
官方注释中的例子:
作者修改后的例子:(例子中的方法返回值有问题,作者写的是 int
实际上应该是 double
)
public class Point {
private int x, y;
final StampedLock sl = new StampedLock();
//计算到原点的距离
double distanceFromOrigin() {
// 乐观读
long stamp = sl.tryOptimisticRead();
// 读入局部变量,读的过程数据可能被修改(将类字段赋值给方法中的局部变量)
int curX = x, curY = y;
// 判断执行读操期间类变量是否被修改过,如果修改过则 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);
}
}
在 distanceFromOrigin()
这个方法中,首先通过调用 tryOptimisticRead()
获取了一个 stamp
,这里的 tryOptimisticRead()
就是我们之前提到的乐观读
。
之后共享变量
x 和 y 读入方法的局部变量
中,需要注意的是:由于 tryOptimisticRead()
是无锁
的,所以共享变量
x 和 y 读入方法局部变量时,这两个值可能已经被其他线程修改
了,因此在最后读完之后,还需要再次验证
是否存在写操作,这个验证操作通过调用 validate(stamp)
实现。如果在执行乐观读期间存在其他线程的写操作导致变量被修改,就将乐观读升级为悲观读锁 —— 这个做法很合理,否则就需要在一个循环里反复执行**乐观读——验证
** 这个过程,直到在这期间没有其他线程修改变量,而循环读会浪费大量的 CPU 时间片,升级为悲观读锁,代码简练且不容易出错,实际工作中也推荐这样做。
进一步理解乐观读
StampedLock
的乐观读锁
和 数据库
的乐观锁
有异曲同工的效果。作者先接触了数据库的乐观锁,然后才接触 JDK 的 StampedLock
类,而他认为学习数据库的乐观锁对于理解 StampedLock
有很大帮助,所以下面是数据库乐观锁
的介绍。
【而我对于数据库乐观锁刚好不是非常了解,学习一波。】
一个数据库乐观锁的使用场景:在 ERP 生产环境中,有多个人通过 ERP 系统提供的 UI 同时修改一条生产订单,如何保证生产订单的数据并发安全?当时作者采用的是数据库乐观锁。
乐观锁的实现很简单,在订单表 product_doc 里增加一个 数值型 的版本号字段 version,每次更新 product_doc 这个表的时候该字段加1。生产订单的 UI 在展示的时候需要查询数据库,此时将这个 version 字段与其他业务字段一起返回给生产订单 UI,假设用户查询的生产订单 id = 777,那么 SQL 语句类似下面这样:
select id,...,version from product_doc where id = 777
用户在生产订单UI
执行保存
操作的时候,后台利用下面的 SQL
语句更新
了生产订单,此处假设该订单的 version=9
:
update product_doc set version=version+1,... where id =777 and version = 9
如果这条 SQL
语句执行成功并且返回的条数等于1
,说明该条数据从查询到保存期间没有其他人修改过这条数据,因为如果数据被修改过 version
一定大于9
.
数据库中的乐观锁
,查询的时候需要将 version
字段查出来,更新的时候利用该字段做验证
,这个字段就类似 StampedLock
中的 stamp
,这样对比来看,更容易理解 StampedLock
里乐观读的用法。
【确实,看完这个例子之后,感觉挺简单的,容易理解。】
StampLock 使用注意事项
对于读多写少的场景,StampedLock
的性能很好,简单应用可以基本替代 ReadWriteLock
,但是 StampedLock
功能仅是 ReadWriteLock
的**子集
**,所以在使用时有几个需要注意
的地方:
StampedLock
命名上没有增加Reentrant
,它是不可重入
的锁,这点在使用中特别需要注意。StampedLock
悲观读锁、写锁都不支持条件变量
。- 如果
线程阻塞
在StampedLock
的readLock()
或者writeLock()
上时,此时调用阻塞线程的interrupt()
方法,会导致CPU 飙升
。
例如下面的代码中,线程 T1获取写锁后将自己阻塞
,线程 T2
尝试获取悲观读锁也会阻塞
。如果此时调用线程 T2
的 interrupt()
方法来中断线程T2
的话,会发现线程T2
所在的 CPU
占用率会飙升到 100%
。
public class CPUErroDemo {
final static StampedLock lock = new StampedLock();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
// 获取写锁
lock.writeLock();
// 永远阻塞在这里,不释放写锁
LockSupport.park();
});
t1.start();
// 保证T1 获取写锁
Thread.sleep(100);
Thread t2 = new Thread(() -> {
// 阻塞在悲观读锁
lock.readLock();
});
t2.start();
// 保证 T2 阻塞在读锁
Thread.sleep(100);
// 中断线程T2,该操作会导致 T2 线程所在 CPU 飙升
t2.interrupt();
t2.join();
}
}
所以,在使用 StampedLock 时一定不要调用中断操作,如果需要支持中断功能,需要使用可中断的悲观读锁
:readLockInterruptibly()
和 写锁
writeLockInterruptibly()
,这个规则需要牢记。
总结:
StampedLock
看上去复杂,但是理解乐观锁背后的原理
之后,使用起来还是比较流畅的,作者建议认真揣摩官方在注释中的例子
,那些基本都是最佳实践
。
以下是作者将官方示例精简后的代码模板,工作中尽量按照这个模板来使用 StampedLock
:
StampedLock 读模板:
final StampedLock sl = new StampedLock();
// 乐观读
void test() {
long stamp = sl.tryOptimisticRead();
// 将共享变量赋值给方法局部变量
...
//校验stamp
if (!sl.validate(stamp)) {
// 升级为悲观读锁
stamp = sl.readLock();
}try {
// 将共享变量赋值给方法局部变量
..
}finally {
// 释放悲观读锁
sl.unlockRead(stamp);
}
}
// 变量的获取完毕,下面就是使用这个局部变量的业务逻辑
StampedLock 写模板:
long stamp = sl.writeLock();
try {
// 写共享变量
...
} finally {
sl.unLockWrite(stamp);
}
Q.E.D.
Comments | 0 条评论