StampedLock是JDK8新增的API,是读写锁ReentrantReadWriteLock优化版,可以用来避免写饥饿。它使用版本戳和读写模式来控制并发访问,加锁会获得一个版本戳,然后解锁需要这个版本戳去匹配,匹配上才可以解锁。由于StampedLock的源码过于复杂,这里就不对源码做详细解读了,只是总结一下他的API特点以及使用场景。
一、三种锁模式
1. 悲观写锁
这是普通的排他锁,只能是一个线程获取到,但是不能重入,重入会引起死锁。看api写锁是不会阻塞,会一直自旋获取锁。调用writeLock()获取普通的写锁,调用tryWriteLock()方法会立即返回获取写锁的结果。
public long writeLock() {
long s, next;
// acquireWrite方法会做轮询获取,过于复杂不做详细解释
return ((((s = state) & ABITS) == 0L &&
U.compareAndSwapLong(this, STATE, s, next = s + WBIT)) ?
next : acquireWrite(false, 0L));
}
// 这个方法会马上返回加锁结果
public long tryWriteLock() {
long s, next;
return ((((s = state) & ABITS) == 0L &&
U.compareAndSwapLong(this, STATE, s, next = s + WBIT)) ?
next : 0L);
}
2.悲观读锁模式
悲观读锁与写锁是互斥,但是读锁与读锁之间可以共享,获取悲观读锁的方法是readLock(),调用这个方法会首先进行自旋获取,如果多次自旋获取不了则阻塞线程。另外可以调用立即返回加锁结果的方法tryReadLock()
public long readLock() {
long s = state, next;
// acquireRead()方法首先会做自旋
return ((whead == wtail && (s & ABITS) < RFULL &&
U.compareAndSwapLong(this, STATE, s, next = s + RUNIT)) ?
next : acquireRead(false, 0L));
}
public long tryReadLock() {
for (;;) {
long s, m, next;
if ((m = (s = state) & ABITS) == WBIT)
return 0L;
else if (m < RFULL) {
if (U.compareAndSwapLong(this, STATE, s, next = s + RUNIT))
return next;
}
// 这个分支会因为读锁数量大于了126个,则做溢出的处理,调用者不用关注这里
else if ((next = tryIncReaderOverflow(s)) != 0L)
return next;
}
}
3.乐观读模式
它适合多读特别少写的场景,注意这个少写,就是整个加乐观锁的过程中,写预计会出现特别少的次数。如果读取的过程中,写的次数过多,那么这个乐观锁其实就会失效,因为乐观锁不是可信的,能够被写锁破坏掉。乐观锁其实没有加锁,只是在读取前,它首先是调用tryOptimisticRead()获取到一个版本戳,然后再读取共享变量,最后在使用这些共享变量时再次验证这个版本戳是否有写锁改过。下面看看官方的例子吧
class Point {
private double x, y;
private final StampedLock sl = new StampedLock();
double distanceFromOrigin() {
// 这里调用乐观锁,从源码中可以看到,它没有加锁,只是获取到一个版本戳
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);
}
}
// 这句执行之前,X、Y可能被改了
return Math.sqrt(currentX * currentX + currentY * currentY);
}
}
public long tryOptimisticRead() {
long s;
return (((s = state) & WBIT) == 0L) ? (s & SBITS) : 0L;
}
// 校验版本戳是否改过
public boolean validate(long stamp) {
U.loadFence();
return (stamp & SBITS) == (state & SBITS);
}
官方文档中表明说乐观锁可减少锁竞争,增加并发的吞吐量,从代码层面确实如此,但是从实际应用中,我觉得这种乐观锁是不够可靠的加锁,使用时要特别注意。
二、API特点
优点
- 有乐观读锁。减少了并发冲突,增大了并发吞吐量,也可以避免写锁饥饿。
- 读写可以相互转换。这减少了代码的开发量,不用先释放占有的锁,然后再去获取另外一种锁。
缺点
- 乐观读是不可靠的。乐观锁严格意义不是锁,只是用版本戳来判断共享资源是否被改过。
- 不能重入,重入会造成死锁。