比读写锁(ReadWriteLock)更快的锁(StampedLock)
区别
ReadWriteLock 支持两种模式:
- 读锁
- 写锁。
而 StampedLock 支持三种模式:
- 写锁
- 悲观读锁(等价于ReadWriteLock 读锁)
- 乐观读
这里可以看到StampedLock在命名上没有Reentrant前缀,所以StampedLock是不可重入锁
可以看到 StampedLock 也是和ReadWriteLock类似有读锁和写锁,也是运行多个线程同时获取悲观读锁,但是只运行一个线程获取写锁,写锁和悲观读锁是互斥的。不过有一点不同的是StampedLock 里的写锁和悲观读锁加锁成功之后,都会返回一个 stamp;然后解锁的时候,需要传入这个 stamp
我们来看看使用案例
// 获取读锁
long stamp = lock.readLock();
try {
// 业务代码
} finally {
lock.unlock(stamp);
}
// 获取写锁
long stamp1 = lock.writeLock();
try {
// 业务代码
} finally {
lock.unlockWrite(stamp1);
}
可以看到使用悲观读锁和写锁本质上和ReadWriteLock没什么区别,那么StampedLock 的性能为什么会比 ReadWriteLock好呢
细心的读者会发现我们一直没有介绍上面的乐观读
StampedLock的乐观读和普通的读锁的区别是:普通的读锁在多个线程同时读的时候,所有的写操作会被阻塞,而 StampedLock 提供的乐观读,是允许一个线程获取写锁的,也就是说不是所有的写操作都被阻塞,也就是说乐观读其实是无锁的。那具体要如何使用呢?我们来看一个简单的例子
private double x, y;
private final StampedLock sl = new StampedLock();
//下面看看乐观读锁案例
double distanceFromOrigin() {
//获得一个乐观读锁
long stamp = sl.tryOptimisticRead();
//将两个字段读入本地局部变量,期间可能被其他线程修改值
double currentX = x, currentY = y;
//判断执行读操作期间,是否存在写操作,如果存在,则 sl.validate 返回 false
if (!sl.validate(stamp)) {
// 升级为悲观读锁
stamp = sl.readLock();
try {
// 重新读取数据
currentX = x;
currentY = y;
} finally {
// 释放悲观读锁
sl.unlockRead(stamp);
}
}
// 注意这里还是会有并发冲突问题,但是使用validate能解决的问题是保证 在读取x, y 的时候值是正确的
// 所以是否使用 StampedLock 还是要看实际业务场景 如果是强一致性还是要加读写锁
return Math.sqrt(currentX * currentX + currentY * currentY);
}
这里可以看到如果存在写操作,就会把乐观读升级为悲观读锁,然后重新读取数据,防止并发问题。看到这里是不是感觉有点鸡肋,不过还是要看有没有业务场景。
可以看到这里使用乐观读锁还是挺麻烦的,这里给出一套模板
final StampedLock sl = new StampedLock();
// 乐观读
long stamp = sl.tryOptimisticRead();
// 读入方法局部变量
......
// 校验stamp
if (!sl.validate(stamp)){
// 升级为悲观读锁
stamp = sl.readLock();
try {
// 读入方法局部变量
.....
} finally {
//释放悲观读锁
sl.unlockRead(stamp);
}
}
//使用方法局部变量执行业务操作
......
不过有一点要注意的是如果线程阻塞在 StampedLock 的 readLock() 或者 writeLock() 上时,此时调用该阻塞线程的 interrupt() 方法,会导致 CPU 飙升。可以看下这个例子
final StampedLock lock = new StampedLock();
Thread t1 = new Thread(() -> {
// 获取写锁
lock.writeLock();
// 永远阻塞在此处,不释放写锁
LockSupport.park();
});
t1.start();
// 保证T1获取写锁
TimeUnit.SECONDS.sleep(100);
//阻塞在悲观读锁
Thread t2 = new Thread(lock::readLock);
t2.start();
// 保证T2阻塞在读锁
TimeUnit.SECONDS.sleep(100);
//中断线程T2
//会导致线程T2所在CPU飙升
t2.interrupt();
t2.join();
不过一般我们都不会这么用,在使用的时候很少有需要响应中断的,如果非要使用。我们查看StampedLock的api会发现他有这么两个方法:悲观读锁 readLockInterruptibly()
和写锁 writeLockInterruptibly()
。所以如果要使用interrupt()方法就将相应的读写锁换成这两个即可。
还没有评论,来说两句吧...