要进一步提升并发执行效率,Java 8引入了新的读写锁:StampedLock。
StampedLock和ReadWriteLock相比,改进之处在于:读的过程中也允许获取写锁后写入!这样一来,我们读的数据就可能不一致,所以,需要一点额外的代码来判断读的过程中是否有写入,这种读锁是一种乐观锁。
乐观锁的意思就是乐观地估计读的过程中大概率不会有写入,因此被称为乐观锁。反过来,悲观锁则是读的过程中拒绝有写入,也就是写入必须等待。显然乐观锁的并发效率更高,但一旦有小概率的写入导致读取的数据不一致,需要能检测出来,再读一遍就行。
public class Point {
private final StampedLock stampedLock = new StampedLock();
private double x;
private double y;
public void move(double deltaX, double deltaY) {
long stamp = stampedLock.writeLock(); // 获取写锁
try {
x += deltaX;
y += deltaY;
} finally {
stampedLock.unlockWrite(stamp); // 释放写锁
}
}
public double distanceFromOrigin() {
long stamp = stampedLock.tryOptimisticRead(); // 获得一个乐观读锁
// 注意下面两行代码不是原子操作
// 假设x,y = (100,200)
double currentX = x;
// 此处已读取到x=100,但x,y可能被写线程修改为(300,400)
double currentY = y;
// 此处已读取到y,如果没有写入,读取是正确的(100,200)
// 如果有写入,读取是错误的(100,400)
if (!stampedLock.validate(stamp)) { // 检查乐观读锁后是否有其他写锁发生
stamp = stampedLock.readLock(); // 获取一个悲观读锁
try {
currentX = x;
currentY = y;
} finally {
stampedLock.unlockRead(stamp); // 释放悲观读锁
}
}
return Math.sqrt(currentX * currentX + currentY * currentY);
}
}
StampedLock提供了乐观读锁,可取代ReadWriteLock以进一步提升并发性能;
StampedLock是不可重入锁。
存在问题:检查版本号后没有发生改变,代码没有进入if语句,但是在return之前有数据写入了,这时后面计算Math.sqrt用的就是旧数据了
1:你再想想,如果刚执行完return语句时,发生了写入,是不是要再读一遍?你怎么知道发生了写入?
锁的作用是保证读的数据逻辑正确,不会读到一部分写入前的数据一部分写入后的数据
2:也就是这里的锁是保证了x和y读取的原子性,不保证整个方法的原子性,可以这么理解吧
3:保证从 tryOptimisticRead 到 validate 之间的代码执行的逻辑完整性,不保证 validate 之后的写入,那个跟你这次读已经无关了
深刻理解引入乐观锁的目的:
其一:解决读写冲突而为了提交并发效率选择的一种机制,
其二是保证读一致性,牺牲处理数据不一致导致脏数据的可能性可以容忍,适用于不是很严谨的业务逻辑过程中,所有保证读数据一致性就是首要原则,不保证读之后的业务逻辑(悲观锁,缓存锁,应用锁都是在锁内的临界代码内完成并发带来的不完整性不一致性的问题)
退出临界代码块也不是保证到return返回结果的
所以说:具体问题具体对待,具体场景具体分析
补充:
检查版本号后没有发生改变,代码没有进入if语句,但是在return之前有数据写入了,这时后面计算Math.sqrt用的就是旧数据了
补充:我的理解是直接return+结果是原子的,
如果return前有计算过程,就不能保证原子性,也就是不能保证整个方法的原子性,这种情况就会有并发处理数据不一致的问题,
假设系统有个共享的int x, int y记录鼠标位置,数据变化:
(100, 200), (110, 205), (120, 209), (130, 212)…
你读的时候锁能保证读的x, y是一致的,不会出现(t1时刻的x, t2时刻的y)
至于你读了之后(x, y)也在继续变化,但你不用关心了因为啥也干不了,总不能一直读不return吧?鼠标位置永远在变化中,要等到什么时候?
读了还要处理呢,比如判断x, y是不是在按钮方框内,在就要高亮显示,处理的时候x,y还在继续变化,但那是下一次读-处理的循环要干的事情,跟你这次读的数据无关。
锁只保证逻辑一致性,释放锁以后的后续更新跟这次读的结果已经无关了。
深入理解加锁的目的
主要保证逻辑一致性就可以,保证整体的逻辑一致性就行,就跟高并发的i++,只要保证i++一致性就行