StampedLock:一个并发编程中非常重要的票据锁

摘要:一起来聊聊这个在高并发环境下比ReadWriteLock更快的锁——StampedLock 。
本文分享自华为云社区《【高并发】一文彻底理解并发编程中非常重要的票据锁——StampedLock》,作者: 冰 河。
什么是StampedLock?ReadWriteLock锁允许多个线程同时读取共享变量,但是在读取共享变量的时候,不允许另外的线程多共享变量进行写操作,更多的适合于读多写少的环境中 。那么,在读多写少的环境中 , 有没有一种比ReadWriteLock更快的锁呢?
答案当然是有!那就是我们今天要介绍的主角——JDK1.8中新增的StampedLock!没错,就是它!
StampedLock与ReadWriteLock相比,在读的过程中也允许后面的一个线程获取写锁对共享变量进行写操作,为了避免读取的数据不一致,使用StampedLock读取共享变量时,需要对共享变量进行是否有写入的检验操作,并且这种读是一种乐观读 。
总之,StampedLock是一种在读取共享变量的过程中,允许后面的一个线程获取写锁对共享变量进行写操作,使用乐观读避免数据不一致的问题,并且在读多写少的高并发环境下,比ReadWriteLock更快的一种锁 。
StampedLock三种锁模式这里,我们可以简单对比下StampedLock与ReadWriteLock,ReadWriteLock支持两种锁模式:一种是读锁,另一种是写锁,并且ReadWriteLock允许多个线程同时读共享变量,在读时,不允许写 , 在写时 , 不允许读 , 读和写是互斥的,所以,ReadWriteLock中的读锁,更多的是指悲观读锁 。
StampedLock支持三种锁模式:写锁、读锁(这里的读锁指的是悲观读锁)和乐观读(很多资料和书籍写的是乐观读锁,这里我个人觉得更准确的是乐观读,为啥呢?我们继续往下看?。?。其中,写锁和读锁与ReadWriteLock中的语义类似,允许多个线程同时获取读锁,但是只允许一个线程获取写锁,写锁和读锁也是互斥的 。
另一个与ReadWriteLock不同的地方在于:StampedLock在获取读锁或者写锁成功后,都会返回一个Long类型的变量,之后在释放锁时,需要传入这个Long类型的变量 。例如,下面的伪代码所示的逻辑演示了StampedLock如何获取锁和释放锁 。
public class StampedLockDemo{ //创建StampedLock锁对象 public StampedLock stampedLock = new StampedLock(); //获取、释放读锁 public void testGetAndReleaseReadLock(){ long stamp = stampedLock.readLock(); try{ //执行获取读锁后的业务逻辑 }finally{ //释放锁 stampedLock.unlockRead(stamp); } } //获取、释放写锁 public void testGetAndReleaseWriteLock(){ long stamp = stampedLock.writeLock(); try{ //执行获取写锁后的业务逻辑 。}finally{ //释放锁 stampedLock.unlockWrite(stamp); } }}StampedLock支持乐观读,这是它比ReadWriteLock性能要好的关键所在 。 ReadWriteLock在读取共享变量时,所有对共享变量的写操作都会被阻塞 。而StampedLock提供的乐观读,在多个线程读取共享变量时,允许一个线程对共享变量进行写操作 。
我们再来看一下JDK官方给出的StampedLock示例 , 如下所示 。
class Point { private double x, y; private final StampedLock sl = new StampedLock(); void move(double deltaX, double deltaY) { // an exclusively locked method long stamp = sl.writeLock(); try {x += deltaX;y += deltaY; } finally { sl.unlockWrite(stamp); } } double distanceFromOrigin() { // A read-only method 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); } } return Math.sqrt(currentX * currentX + currentY * currentY); } void moveIfAtOrigin(double newX, double newY) { // upgrade // Could instead start with optimistic, not read mode long stamp = sl.readLock(); try { while (x == 0.0 && y == 0.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); } }}在上述代码中 , 如果在执行乐观读操作时,另外的线程对共享变量进行了写操作,则会把乐观读升级为悲观读锁 , 如下代码片段所示 。
double distanceFromOrigin() { // A read-only method //乐观读 long stamp = sl.tryOptimisticRead(); double currentX = x, currentY = y; //判断是否有线程对变量进行了写操作 //如果有线程对共享变量进行了写操作 //则sl.validate(stamp)会返回false if (!sl.validate(stamp)) { //将乐观读升级为悲观读锁stamp = sl.readLock(); try { currentX = x; currentY = y; } finally { //释放悲观锁 sl.unlockRead(stamp); } } return Math.sqrt(currentX * currentX + currentY * currentY);}这种将乐观读升级为悲观读锁的方式相比一直使用乐观读的方式更加合理 , 如果不升级为悲观读锁,则程序会在一个循环中反复执行乐观读操作,直到乐观读操作期间没有线程执行写操作 , 而在循环中不断的执行乐观读会消耗大量的CPU资源,升级为悲观读锁是更加合理的一种方式 。

推荐阅读