# 简介
一般应用,都是读多写少, ReentrantReadWriteLock
因读写互斥,故读时阻塞写,因而性能上上不去。可能会使写线程饥饿, StampedLock
营运而生。
``StampedLock 并没有使用
AQS ,
API 相对复杂,内壁实现比
ReentrantReadWriteLock 复杂得多,所以本文就不对源码进行过多解释。有点就是吞吐量相对
ReentrantReadWriteLock` 有显著提升。
# 原理
获取锁的方法都会返回一个邮戳 Stamp
, Stamp=0
表示获取失败,其余都表示成功。释放锁时,也需要一个邮戳 Stamp
,这个 Stamp
必须和获得锁时的 Stamp
一致。
看一个简单的例子
class Point { | |
private double x, y; | |
private final StampedLock sl = new StampedLock(); | |
void move(double deltaX, double deltaY) { | |
long stamp = sl.writeLock(); | |
try { | |
x += deltaX; | |
y += deltaY; | |
} finally { | |
sl.unlockWrite(stamp);// 用到了 Stamp 释放锁 | |
} | |
} | |
} |
# 特点
StampedLock
不支持锁重入,写锁的重入会造成死锁。但是支持三种模式:写锁,悲观锁,乐观锁。- 只允许一个线程获取写锁,写锁和悲观读锁是互斥的。
- 允许多 个线程同时获取乐观锁和悲观读锁。
# 使用
我们先来看一下 JDK1.8
源码自带的案例
public class StampedLockDemo { | |
private double x,y; | |
private final StampedLock sl = new StampedLock(); | |
//【写锁 (排它锁)】 | |
void move(double deltaX,double deltaY) { | |
/**stampedLock 调用 writeLock 和 unlockWrite 时候都会导致 stampedLock 的 stamp 值的变化 | |
* 即每次 + 1(不是狭义的加一),直到加到最大值,然后从 0 重新开始 | |
**/ | |
long stamp =sl.writeLock(); // 写锁 | |
try { | |
x +=deltaX; | |
y +=deltaY; | |
} finally { | |
sl.unlockWrite(stamp);// 释放写锁 | |
} | |
} | |
//【乐观读锁】 | |
double distanceFromOrigin() { // A read-only method | |
/** | |
* tryOptimisticRead 是一个乐观的读,使用这种锁的读不阻塞写 | |
* 每次读的时候得到一个当前的 stamp 值(类似时间戳的作用) | |
*/ | |
long stamp = sl.tryOptimisticRead(); | |
// 这里就是读操作,读取 x 和 y,因为读取 x 时,y 可能被写了新的值,所以下面需要判断 | |
double currentX = x, currentY = y; | |
/** 如果读取的时候发生了写,则 stampedLock 的 stamp 属性值会变化,此时需要重读, | |
* 再重读的时候需要加读锁(并且重读时使用的应当是悲观的读锁,即阻塞写的读锁) | |
* 当然重读的时候还可以使用 tryOptimisticRead,此时需要结合循环了,即类似 CAS 方式 | |
* 读锁又重新返回一个 stampe 值 */ | |
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) { // upgrade | |
// 读锁(这里可用乐观锁替代) | |
long stamp = sl.readLock(); | |
try { | |
// 循环,检查当前状态是否符合 | |
while (x == 0.0 && y == 0.0) { | |
/** | |
* 转换当前读戳为写戳,即上写锁 | |
* 1. 写锁戳,直接返回写锁戳 | |
* 2. 读锁戳且写锁可获得,则释放读锁,返回写锁戳 | |
* 3. 乐观读戳,当立即可用时返回写锁戳 | |
* 4. 其他情况返回 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); | |
} | |
} | |
} |
# 参考
灰信网:https://www.freesion.com/article/1706212555/
Java 全栈知识体系:https://pdai.tech/md/java/java8/java8-stampedlock.html
https://wizardforcel.gitbooks.io/java8-new-features/content/10.html
https://wizardforcel.gitbooks.io/java8-tutorials/content/Java 8 并发教程 Threads 和 Executors.html