简述:在上篇文章中介绍和分析了ReentranReadWriteLock,我们发现是可以将读写分别进行加锁,需要注意的一点就是,当读锁获取到资源的时候,如果要进行写入,不管是否是当前同一个线程都不可以,在开发中我们大部分场景其实都是读多写少,显然,如果要进行写入,需要等待所有的读操作都执行完,显然性能上就会有所降低,因此,在JDK1.8里面提供了StampedLock这个类,他里面提供了一种乐观锁读,很大程度上满足了,读多写少的场景。
StampedLock介绍:在每次加锁之后,都会返回一个邮戳,来表示锁的版本,在释放的时候,需要把这个锁的版本传递进去
官方示例代码:
下面是他的一个基础类,包含两个元素,x和y, 及一个stampedLock锁
class Point {
private double x, y;
private final StampedLock sl = new StampedLock();
}
写锁-排他锁
void move(double deltaX, double deltaY) { // an exclusively locked method
long stamp = sl.writeLock();
try {
x += deltaX;
y += deltaY;
} finally {
sl.unlockWrite(stamp);
}
}
在同一时刻只能有一个线程进行move操作,其他的读线程和写线程都会进行阻塞,获取锁之后返回一个邮戳,释放的时候,也基于邮戳进行释放。
乐观读锁:
double distanceFromOrigin() { // A read-only method
//尝试获取乐观锁
long stamp = sl.tryOptimisticRead();
//将变量拷贝到方法体栈内
double currentX = x, currentY = y;
//检查在获取到读锁票据后,锁有没被其他写线程排它性抢占
if (!sl.validate(stamp)) {
// 如果被抢占则获取一个共享读锁(悲观获取)
stamp = sl.readLock();
try {
// 将全部变量拷贝到方法体栈内
currentX = x;
currentY = y;
} finally {
// 释放共享读锁
sl.unlockRead(stamp);
}
}
// 返回计算结果(7)
return Math.sqrt(currentX * currentX + currentY * currentY);
}
1、获取乐观读锁
2、将变量拷贝到方法栈内
3、检查所是否失效,即在这个过程中是否有其他写线程修改了数据,否则执行 6
4、如果失效,获取悲观读锁,然后更新方法栈内的变量
5、释放掉悲观读锁,
6、进行计算
Q: 是否可以在获取到乐观锁之后,判断锁是否失效,如果没有失效再进行变量拷贝?
A: 不可以。因为在获取到乐观锁之后,判断乐观锁并未失效,但是在变量拷贝的过程中,其他写线程持有锁,对x和y其中的一个进行修改,最终计算的数据并不能保证一致性。
悲观读锁:
// 使用悲观锁获取读锁,并尝试转换为写锁
void moveIfAtOrigin(double newX, double newY) { // upgrade
// Could instead start with optimistic, not read mode
// 这里可以使用乐观读锁替换
long stamp = sl.readLock();
try {
// 如果当前点在原点则移动
while (x == 0.0 && y == 0.0) {
// 尝试将获取的读锁升级为写锁
long ws = sl.tryConvertToWriteLock(stamp);
// 升级成功,则更新票据,并设置坐标值,然后退出循环
if (ws != 0L) {
stamp = ws;
x = newX;
y = newY;
break;
}
else {
// 读锁升级写锁失败则释放读锁,显示获取独占写锁,然后循环重试
sl.unlockRead(stamp);
stamp = sl.writeLock();
}
}
} finally {
sl.unlock(stamp);
}
}
1、先获取到悲观读锁,
2、判断当前数据是否在原点,如果是,尝试将锁转为写锁,
3、如果获取写锁成功,即返回的邮戳不等于0 ,对数据进行赋值,然后跳出循环
4、如果尝试升级写锁失败,先释放掉读锁,然后再转为获取独享写锁,等待其他线程执行结束
5、最终释放掉锁
对于乐观锁的使用步骤:
1、获取到乐观锁
2、将变量拷贝至方法栈内
3、判断乐观锁是否失效,如果失效执行4,否则执行7
4、获取悲观读取锁,再次拷贝变量
5、释放掉悲观读锁
7、执行计算结果
关于StampedLock中的一个bug:
StampedLock其内部是通过死循环+CAS操作的方式来修改状态位,在挂起线程时,是通过unsafe.park的方式,而对于中断的线程,unsafe.park会直接返回,而在StampedLock的死循环逻辑中,没有处理中断的逻辑,就会导致阻塞在park上的线程中断后,再次进入循环,直到当前死循环满足退出条件,因此整个过程会使cpu暴涨。
解决办法:
在acquireRead(boolean interruptible, long deadline) 和acquireWrite(boolean interruptible, long deadline)中添加 保存/复原中断状态的机制。
以下是github上大神对StampedLock.java类的修改 点击打开链接
参考:JDK8中StampedLock原理探究
《Java高并发程序设计》学习 --6.6 读写锁的改进:StampedLock
【Java并发】- StampedLock实现浅析
Bug:StampedLock的中断问题导致CPU爆满