比读写锁更快的StampedLock
2. StampedLock
2.1 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提供的乐观读,当多个线程在读时,允许一个线程获得写锁!
2.2 StampedLock乐观读原理
下面这段代码是出自 Java SDK 官方示例,并略做了修改。
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);
}
}
上面的代码中,如果执行乐观读期间存在写操作,会把乐观读升级为悲观读锁,否则需要一个循环反复执行乐观读,直到期间没有写操作,循环会浪费cpu资源,所以升级为悲观读锁,合情合理!
注意:在上面代码中validate校验期间没有写操作,但是如果在执行Math.sqrt之前,校验之后,进行写操作,还是不能及时判断数据的同步,只能保证正确性和一致性。这是我有点不明白的地方,如果有哪位老师明白的,希望可以写在评论区留言。
其实在提到乐观读,你可能会想到数据库事务MVCC(多版本并发控制),其实两者有点相似,按照MVCC原理,数据的每次修改都对应一个版本号,不存在只修改数据或者版本号。
在数据库事务开启的时候,会给数据库打一个快照,以后再该事务中所有的读写操作都是基于这个快照的,当提交事务的时候,也就时写入数据库数据时,会校验写入数据版本是否与读数据时的版本号一致,如果一致,就是所有读写过的数据在该事务执行期间没有发生变化,就提交数据,如果不一致,发生变化了,说明在该事务执行期间,其他事务提交了,产生冲突,不能提交。
乐观读的想法是,没事,没有写操作,校验一下是否存在写操作,没有,操作数据;有,升级悲观锁。
MVCC的想法是,没事,没有写操作,复制一份,先修改复制的,在写入数据库时,(检验版本看是否存在其他写操作,有,不能提交事务,没有,提交事务。)原子操作
2.3 StampedLock注意事项
对于读多写少的场景 StampedLock 性能很好,简单的应用场景基本上可以替代 ReadWriteLock,但是 StampedLock 的功能仅仅是 ReadWriteLock 的子集,在使用的时候,还是有几个地方需要注意一下。
- StampedLock 在命名上并没有增加 Reentrant,所以,StampedLock 不支持重入。
- 另外,StampedLock 的悲观读锁、写锁都不支持条件变量,这个也需要你注意。
- 如果线程阻塞在 StampedLock 的 readLock() 或者 writeLock() 上时,此时调用该阻塞线程的 interrupt() 方法,会导致 CPU 飙升。所以,使用 StampedLock 一定不要调用中断操作,如果需要支持中断功能,一定使用可中断的悲观读锁 readLockInterruptibly() 和写锁 writeLockInterruptibly()。这个规则一定要记清楚。
例如下面的代码示例(中断阻塞的readLock导致cpu飙升):
final StampedLock lock = new StampedLock();
Thread T1 = new Thread(()->{
// 获取写锁
lock.writeLock();
LockSupport.park(); // 永远阻塞在此处,不释放写锁
});
T1.start();
// 保证T1获取写锁
Thread.sleep(100);
Thread T2 = new Thread(()->
lock.readLock() //阻塞在悲观读锁
);
T2.start();
Thread.sleep(100);// 保证T2阻塞在读锁
T2.interrupt();//中断线程T2//会导致线程T2所在CPU飙升
T2.join();
2.4 标准StampedLock 读写模板
建议你在实际工作中尽量按照下面的模板来使用 StampedLock。
- StampedLock 读模板:
final StampedLock sl = new StampedLock();
// 乐观读
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);
}