浅谈Java中的 ReadWriteLock 与 StampedLock
认识 ReadWriteLock (读写锁) ✨
上篇文章学习了 ReentrantLock(可重入锁) 保证了单个线程可执行代码,在任何时刻,仅允许单个线程修改数据。但很多项目实际场景,需要多个线程同时读某个数据,且仅有一个线程在写数据,其他线程就必须等待。
使用 ReadWriteLock 就可解决这个问题
- 只允许一个线程写入(其他线程既不能写入也不能读取);
- 没有写入时,多个线程允许同时读(提高性能)
读写锁示例
public class Counter {
private final ReadWriteLock rwlock = new ReentrantReadWriteLock(); // 创建读写锁对象
private final Lock rlock = rwlock.readLock(); // 读锁
private final Lock wlock = rwlock.writeLock(); // 写锁
private int[] counts = new int[10];
public void inc(int index) {
wlock.lock(); // 加写锁
try {
counts[index] += 1;
} finally {
wlock.unlock(); // 释放写锁
}
}
public int[] get() {
rlock.lock(); // 加读锁
try {
return Arrays.copyOf(counts, counts.length);
} finally {
rlock.unlock(); // 释放读锁
}
}
}
读写操作分别用 读锁 和 写锁 来同步,多个线程可以同时获得读锁,就大大提高了并发读的效率。
使用ReadWriteLock时,适用场景是同一个数据,有大量线程读取,但仅有少数线程修改它。
举例一个场景,回复论坛帖子,回复操作可以看做写操作,是不频繁的,但浏览可以看做读操作,是非常频繁的,此种场景就可使用 ReadWriteLock。
认识 StampedLock(新读写锁) ✨
ReadWriteLock (读写锁)可以解决多线程同时读取,但仅一个线程能写入的问题。
有个潜在问题:如果有线程正在读,写线程需要等读线程释放锁后才能获取到写锁,即读的过程中是不允许写,是一种悲观的读锁。
在Java8 中,引入了一种新读写锁: 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);
}
}
与 ReadWriteLock 相比,写入时加锁是完全一样的,不同的是读取操作。
先通过 tryOptimisticRead() 获取一个乐观读锁,返回版本号。随后进行读取,读取完成,通过 validate() 去验证版本号;
可见,StampedLock 把读锁细分为乐观读和悲观读,能进一步提升并发效率。当然这也有代价的:代码更加复杂,StampedLock 是不可重入锁,不能在单个线程中反复获取同一个锁。
StampedLock 提供了将悲观读锁升级为写锁的功能,它主要使用在if-then-update的场景:即先读,如果读的数据满足条件,就返回,如果读的数据不满足条件,再尝试写。
总结
ReadWriteLock 小结
使用 ReadWriteLock 可以提高读取效率:
- ReadWriteLock只允许一个线程写入
- ReadWriteLock允许多个线程在没有写入时同时读取
- ReadWriteLock适合读多写少的场景
StampedLock 小结
- StampedLock 提供了乐观读锁,可取代 ReadWriteLock 进一步提升并发性能
- StampedLock 是不可重入锁
转载自:https://juejin.cn/post/7140293309196402724