从多线程到分布式(四)——高性能读写锁StampedLock
2023-08-10 本文已影响0人
吟游雪人
乐观锁
如果并发产生冲突的概率很低,就不必使用悲观锁,而是使用乐观锁。所谓“乐观”,就是假定冲突的概率很低,所以它采用的“加锁”方式是,先修改完共享资源,再验证这段时间内有没有发生冲突。如果没有其他线程在修改资源,那么操作完成。如果发现其他线程已经修改了这个资源,就放弃本次操作。
所以,同样是CAS来实现,先获取锁的,就叫悲观自旋锁;先修改资源的,就叫乐观锁。
另类的性能锁StamperdLock
说他另类,是因为
1.StampedLock 的悲观读锁、写锁都不支持条件变量
2.不支持重入
3.StampedLock 一定不要调用中断操作(这个感觉算是bug)
之所以会有这些特性,是因为包括ReentrantReadWriteLock,ReentrantLock和信号量等同步工具,都是基于AQS同步框架实现的。而在StampedLock中摒弃了AQS框架,为StampedLock实现提供了更多的灵活性。
性能为王的情况下,也就无需实现公平性,自然也就是非公平锁。
final StampedLock sl =
new StampedLock();
// 获取 / 释放悲观读锁示意代码
long stamp = sl.readLock();
try {
// 省略业务相关代码
} finally {
sl.unlockRead(stamp);
}
// 获取 / 释放写锁示意代码
long stamp = sl.writeLock();
try {
// 省略业务相关代码
} finally {
sl.unlockWrite(stamp);
}
参操文档的使用示例
class Point {
//共享的变量x和y
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);
}
}
validate()方法接受参数是上次锁操作返回的邮戳,如果在调用validate()之前,这个锁没有写锁申请过,那就返回true,这也表示锁保护的共享数据并没有被修改
反之,如果锁在validate()之前有写锁申请成功过,那就表示,之前的数据读取和写操作冲突了,程序需要进行重试,或者升级为悲观锁。
可以看到,StamperdLock使用起来比一般的重入锁麻烦的多,之所以使用乐观锁,主要是因为性能问题,在低竞争的环境下,乐观锁的性能会高很多。