在上一篇《你真的懂ReentrantReadWriteLock吗?》中我给大家留了一个引子,一个更高效同时可以避免写饥饿的读写锁---StampedLock。StampedLock实现了不仅多个读不互相阻塞,同时在读操作时不会阻塞写操作。
为什么StampedLock这么神奇?能够达到这种效果,它的核心思想在于,在读的时候如果发生了写,应该通过重试的方式来获取新的值,而不应该阻塞写操作。这种模式也就是典型的无锁编程思想,和CAS自旋的思想一样。这种操作方式决定了StampedLock在读线程非常多而写线程非常少的场景下非常适用,同时还避免了写饥饿情况的发生。这篇文章将通过以下几点来分析StampedLock。
StampedLock的官方使用示例分析
源码分析:读写锁共享的状态量
源码分析:写锁的释放和获取
源码分析:悲观读锁的释放和获取
性能测试
StampedLock的官方使用示例分析先来看一个官方给出的StampedLock使用案例:
public class Point { private double x, y; private final StampedLock stampedLock = new StampedLock(); //写锁的使用 void move(double deltaX, double deltaY){ long stamp = stampedLock.writeLock(); //获取写锁 try { x += deltaX; y += deltaY; } finally { stampedLock.unlockWrite(stamp); //释放写锁 } } //乐观读锁的使用 double distanceFromOrigin() { long stamp = stampedLock.tryOptimisticRead(); //获得一个乐观读锁 double currentX = x; double currentY = y; if (!stampedLock.validate(stamp)) { //检查乐观读锁后是否有其他写锁发生,有则返回false stamp = stampedLock.readLock(); //获取一个悲观读锁 try { currentX = x; } finally { stampedLock.unlockRead(stamp); //释放悲观读锁 } } return Math.sqrt(currentX*currentX + currentY*currentY); } //悲观读锁以及读锁升级写锁的使用 void moveIfAtOrigin(double newX,double newY) { long stamp = stampedLock.readLock(); //悲观读锁 try { while (x == 0.0 && y == 0.0) { long ws = stampedLock.tryConvertToWriteLock(stamp); //读锁转换为写锁 if (ws != 0L) { //转换成功 stamp = ws; //票据更新 x = newX; y = newY; break; } else { stampedLock.unlockRead(stamp); //转换失败释放读锁 stamp = stampedLock.writeLock(); //强制获取写锁 } } } finally { stampedLock.unlock(stamp); //释放所有锁 } } }首先看看第一个方法move,可以看到它和ReentrantReadWriteLock写锁的使用基本一样,都是简单的获取释放,可以猜测这里也是一个独占锁的实现。需要注意的是 在获取写锁是会返回个只long类型的stamp,然后在释放写锁时会将stamp传入进去。这个stamp是做什么用的呢?如果我们在中间改变了这个值又会发生什么呢?这里先暂时不做解释,后面分析源码时会解答这个问题。
第二个方法distanceFromOrigin就比较特别了,它调用了tryOptimisticRead,根据名字判断这是一个乐观读锁。首先什么是乐观锁?乐观锁的意思就是先假定在乐观锁获取期间,共享变量不会被改变,既然假定不会被改变,那就不需要上锁。在获取乐观读锁之后进行了一些操作,然后又调用了validate方法,这个方法就是用来验证tryOptimisticRead之后,是否有写操作执行过,如果有,则获取一个读锁,这里的读锁和ReentrantReadWriteLock中的读锁类似,猜测也是个共享锁。
第三个方法moveIfAtOrigin,它做了一个锁升级的操作,通过调用tryConvertToWriteLock尝试将读锁转换为写锁,转换成功后相当于获取了写锁,转换失败相当于有写锁被占用,这时通过调用writeLock来获取写锁进行操作。
看过了上面的三个方法,估计大家对怎么使用StampedLock有了一个初步的印象。下面就通过对StampedLock源码的分析来一步步了解它背后是怎么解决锁饥饿问题的。
源码分析:读写锁共享的状态量从上面的使用示例中我们看到,在StampedLock中,除了提供了类似ReentrantReadWriteLock读写锁的获取释放方法,还提供了一个乐观读锁的获取方式。那么这三种方式是如何交互的呢?根据AQS的经验,StampedLock中应该也是使用了一个状态量来标志锁的状态。通过下面的源码可以证明这点:
// 用于操作state后获取stamp的值 private static final int LG_READERS = 7; private static final long RUNIT = 1L; //0000 0000 0001 private static final long WBIT = 1L << LG_READERS; //0000 1000 0000 private static final long RBITS = WBIT - 1L; //0000 0111 1111 private static final long RFULL = RBITS - 1L; //0000 0111 1110 private static final long ABITS = RBITS | WBIT; //0000 1111 1111 private static final long SBITS = ~RBITS; //1111 1000 0000 //初始化时state的值 private static final long ORIGIN = WBIT << 1; //0001 0000 0000 //锁共享变量state private transient volatile long state; //读锁溢出时用来存储多出的毒素哦 private transient int readerOverflow;