一、StampedLock
1.1、简述
StampedLock
是Java8引入的一种新的所机制,可以认为它是读写锁ReentrantReadWriteLock
的一个改进版本,读写锁虽然分离了读和写的功能,使得读与读之间可以完全并发,但是读和写之间依然是冲突的,读锁会完全阻塞写锁,它使用的依然是悲观的锁策略。如果有大量的读线程,也有可能引起写线程的饥饿。而StampedLock则提供了一种乐观的读策略,这种乐观策略的锁非常类似于无锁的操作,使得乐观锁完全不会阻塞写线程。
核心思想:StampedLock读写锁中不仅多个读不互相阻塞,同时在读操作时不会阻塞写操作。在读的时候如果发生了写,应该通过重试的方式来获取新的值,而不应该阻塞写操作。这种模式也就是典型的无锁编程思想,和CAS自旋的思想一样。这种操作方式决定了StampedLock在读多写少的场景下非常适用,同时还避免了写饥饿情况的发生。
1.2、读不阻塞写的思路
在读的时候如果发生了写,则应当重读而不是在读的时候直接阻塞写。因为在读多而写比较少的情况下,写线程可能发生饥饿现象,也就是因为大量的读线程存在并且读线程都阻塞写线程,因此写线程可能很少被调度成功。当读执行的时候另一个线程执行了写,则读线程发现数据不一致则执行重读即可。所以读写都存在的情况下,使用StampedLock就可以实现一种无障碍操作,即读写之间不会阻塞对方,但是写和写之间还是阻塞的。
二、StampedLock实现思想
在StampedLock中使用了CLH自旋锁,如果发生了读失败,不立刻把读线程挂起,锁当中维护了一个等待线程队列。所有申请锁但是没有成功的线程都会记录到这个队列中,每一个节点(一个节点表示一个线程)保存一个标记位(locked),用于判断当前线程是否已经释放锁。当一个未标记到队列中的线程试图获得锁时,会取得当前等待队列尾部的节点作为其前序节点,并使用类似如下代码(一个空的死循环)判断前序节点是否已经成功的释放了锁:
while(pred.locked){
}
解释:pred表示当前试图获取锁的线程的前序节点,如果前序节点没有释放锁,则当前线程就执行该空循环并不断判断前序节点的锁释放,即类似一个自旋锁的效果,避免被系统挂起。当循环一定次数后,前序节点还没有释放锁,则当前线程就被挂起而不再自旋,因为空的死循环执行太多次比挂起更消耗资源。
三、StampedLock与ReentrantReadWriteLock对比
3.1、优点
StampedLock锁相对ReentrantReadWriteLock的改进就是加了乐观读, 这样了避免了ReentrantReadWriteLock非公平锁时读多写少, 写入队以后一直轮不到写线程造成写饿死的情况, 还提高了吞吐量。
3.2、缺点
StampedLock锁和ReentrantReadWriteLock锁相比的缺点如下所示:
- 不支持条件变量
- 不能中断线程
- 不能重入
四、StampedLock示例代码
public class Point {
private double x, y;
// 定义StampedLock锁
private final StampedLock stampedLock = new StampedLock();
// 方法一:写锁的使用
void move(double deltaX, double deltaY){
// 获取写锁
long stamp = stampedLock.writeLock();
try {
x += deltaX;
y += deltaY;
} finally {
// 释放写锁
stampedLock.unlockWrite(stamp);
}
}
// 方法二:乐观读锁的使用
double distanceFromOrigin() {
// 获得一个乐观读锁
long stamp = stampedLock.tryOptimisticRead();
double currentX = x;
double currentY = y;
// 检查乐观读锁后是否有其他写锁发生,有则返回false
if (!stampedLock.validate(stamp)) {
// 获取一个悲观读锁
stamp = stampedLock.readLock();
try {
currentX = x;
} finally {
// 释放悲观读锁
stampedLock.unlockRead(stamp);
}
}
return Math.sqrt(currentX*currentX + currentY*currentY);
}
// 方法三:悲观读锁以及读锁升级写锁的使用
void moveIfAtOrigin(double newX,double newY) {
// 悲观读锁
long stamp = stampedLock.readLock();
try {
while (x == 0.0 && y == 0.0) {
// 读锁转换为写锁
long ws = stampedLock.tryConvertToWriteLock(stamp);
// 条件判断是否转换成功
if (ws != 0L) {
// 锁升级
stamp = ws;
x = newX;
y = newY;
break;
} else {
// 转换失败释放读锁
stampedLock.unlockRead(stamp);
// 强制获取写锁
stamp = stampedLock.writeLock();
}
}
} finally {
// 释放所有锁
stampedLock.unlock(stamp);
}
}
}
参考文献
[1].https://blog.csdn.net/ryo1060732496/article/details/109253430
[2].https://www.jianshu.com/p/27b61935b07a
[3].https://www.cnblogs.com/Booker808-java/p/8724598.html
[4].https://www.cnblogs.com/ten951/p/6590579.html