ReentrantReadWriteLock
现实中有这样一种场景:对共享资源有读和写的操作,且写操作没有读操作那么频繁。在没有写操作的时候,多个线程同时读一个资源没有任何问题,所以应该允许多个线程同时读取共享资源;但是如果一个线程想去写这些共享资源,就不应该允许其他线程对该资源进行读和写的操作了。
针对这种场景,JAVA 的并发包提供了读写锁 ReentrantReadWriteLock,它表示两个锁,一个是读操作相关的锁,称为共享锁;一个是写相关的锁,称为排他锁。
读读共享,其他都互斥,描述如下:
- 线程进入读锁的前提条件:
- 没有其他线程的写锁,
- 没有写请求或者有写请求,但调用线程和持有锁的线程是同一个。
- 线程进入写锁的前提条件:
- 没有其他线程的读锁
- 没有其他线程的写锁
使用
ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();
// 获取写锁
ReentrantReadWriteLock.WriteLock writeLock = reentrantReadWriteLock.writeLock();
// 获取读锁
ReentrantReadWriteLock.ReadLock readLock = reentrantReadWriteLock.readLock();
// 写锁加锁和解锁
writeLock.lock();
writeLock.unlock();
// 读锁加锁和解锁
readLock.lock();
readLock.unlock();
StampedLock
StampedLock 的主要特点概括一下,有以下几点:
- 所有获取锁的方法,都返回一个邮戳(Stamp),Stamp 为0表示获取失败,其余都表示成功;
- 所有释放锁的方法,都需要一个邮戳(Stamp),这个 Stamp 必须是和成功获取锁时得到的 Stamp 一致;
- StampedLock 是不可重入的;(如果一个线程已经持有了写锁,再去获取写锁的话就会造成死锁)
- StampedLock 有三种访问模式:
- Reading(读模式):功能和ReentrantReadWriteLock的读锁类似
- Writing(写模式):功能和ReentrantReadWriteLock的写锁类似
- Optimistic reading(乐观读模式):这是一种优化的读模式。
- StampedLock 支持读锁和写锁的相互转换
我们知道 RRW 中,当线程获取到写锁后,可以降级为读锁,但是读锁是不能直接升级为写锁的。
StampedLock 提供了读锁和写锁相互转换的功能,使得该类支持更多的应用场景。- 无论写锁还是读锁,都不支持 Conditon 等待
我们知道,在 ReentrantReadWriteLock 中,当读锁被使用时,如果有线程尝试获取写锁,该写线程会阻塞。
但是,在 Optimistic reading 中,即使读线程获取到了读锁,写线程尝试获取写锁也不会阻塞,这相当于对读模式的优化,但是可能会导致数据不一致的问题。所以,当使用 Optimistic reading 获取到读锁时,必须对获取结果进行校验。
// 成员变量
private double x, y;
// 锁实例
private final StampedLock sl = new StampedLock();
// 排它锁-写锁(writeLock)
void move(double deltaX, double deltaY) {
long stamp = sl.writeLock();
try {
x += deltaX;
y += deltaY;
} finally {
sl.unlockWrite(stamp);
}
}
// 一个只读方法
// 其中存在乐观读锁到悲观读锁的转换
double distanceFromOrigin() {
// 尝试获取乐观读锁
long stamp = sl.tryOptimisticRead();
// 将全部变量拷贝到方法体栈内
double currentX = x, currentY = y;
// 检查在获取到读锁stamp后,锁有没被其他写线程抢占
if (!sl.validate(stamp)) {
// 如果被抢占则获取一个共享读锁(悲观获取)
stamp = sl.readLock();
try {
// 将全部变量拷贝到方法体栈内
currentX = x;
currentY = y;
} finally {
// 释放共享读锁
sl.unlockRead(stamp);
}
}
// 返回计算结果
return Math.sqrt(currentX * currentX + currentY * currentY);
}
// 获取读锁,并尝试转换为写锁
void moveIfAtOrigin(double newX, double newY) {
long stamp = sl.tryOptimisticRead();
try {
// 如果当前点在原点则移动
while (x == 0.0 && y == 0.0) {
// 尝试将获取的读锁升级为写锁
long ws = sl.tryConvertToWriteLock(stamp);
// 升级成功,则更新stamp,并设置坐标值,然后退出循环
if (ws != 0L) {
stamp = ws;
x = newX;
y = newY;
break;
} else {
// 读锁升级写锁失败则释放读锁,显示获取独占写锁,然后循环重试
sl.unlockRead(stamp);
stamp = sl.writeLock();
}
}
} finally {
sl.unlock(stamp);
}
}