读写锁的增加版--StampedLock

182 阅读4分钟

读写锁的增加版--StampedLock

前言

读写锁ReentrantReadWriteLock的优势比一般互斥锁大了不少,因为它支持读读不互斥,读写互斥,写写互斥,对于读多写少的场景简直是yyds,是不是读写锁在读多写少的场景就独霸一方了呢?当然不会,JDK1.8推出了读写锁的增强版StampedLock。

什么是StampedLock

StampedLock依旧是一种读写锁,只是它对比ReentrantReadWriteLock将性能进行了进一步优化。

ReentrantReadWriteLock支持两种模式读锁和写锁,而它的增强版StampedLock支持三种模式悲观读锁、写锁以及乐观读,其中悲观读锁,写锁和ReentrantReadWriteLock的读锁,写锁是及其类似的,也是支持并发读,同一时刻只允许一个线程获取读锁,不同点在于StampedLock获取悲观读锁或写锁后会返回一个long类型的stamp,如下代码所示

 StampedLock stampedLock = new StampedLock();
 ​
 // 获取读锁
 long readStamped = stampedLock.readLock();
 // 读锁解锁
 stampedLock.unlockRead(readStamped);
 ​
 // 获取写锁
 long writeStamped = stampedLock.writeLock();
 // 写锁解锁
 stampedLock.unlockWrite(writeStamped);

解锁时需要将stamp值传入解锁方法,不然解锁失败抛出异常IllegalMonitorStateException

还有一点需要注意的是,StampedLock多出的一种模式为乐观读,这里并没有锁,也就是说这是一种无锁模式,自然在效率上高于ReentrantReadWriteLock。

乐观读的简单使用

乐观读的无锁模式,如果应用得当将是提升效率的利器,以JAVA SDK官方示例展示如下。

 class Point {
     private int x, y;
     public Point(int x,int y){
         this.x = x;
         this.y = y;
     }
     // 省略set get方法
     private final StampedLock sl = new StampedLock();
     // 计算到原点的距离
     public double distanceFromOrigin() {
         // 获取乐观读 注意注意这个不是锁,所以不用解锁
         long stamp = sl.tryOptimisticRead();
         // 读入局部变量,读的过程数据可能被修改
         int curX = x, curY = y;
         // 判断执行读操作期间是否存在写操作,如果存在则sl.validate返回false
         if (!sl.validate(stamp)){
             // 升级为悲观读锁
             stamp = sl.readLock();
             try {
                 curX = x;
                 curY = y;
             } finally {
                 //释放悲观读锁
                 sl.unlockRead(stamp);
             }
         }
         // Math.sqrt 返回一个数的平方根
         return Math.sqrt(curX * curX + curY * curY);
     }
 }

在执行上诉代码过程中,如果获取乐观读后有写操作,可以由sl.validate(stamp)得出,存在则将乐观读升级为悲观读锁,这样避免了需要循环验证是否在乐观读期间存在写操作的情况,节省CPU的消耗,提升效率。

进一步了解乐观读

乐观读这个思想其实类似数据库中的乐观锁,乐观的认为当前的修改是合法的,乐观锁的实现也是非常简单,就是在业务表中新增一个version字段,该字段为数字类型,每次查询就将version字段返回给前端如下所示。

 select id,...,version from testtable where id="***"

每次修改就需要将查询时返回给前端的version字段,传入后台中,能够查询到数据后需要将version的值加一,如下所示。

 update testtable set name="***",version=version+1 where id="***" and version=9

如果修改的数据返回影响行数是一,那么证明在页面查询到页面更改数据期间都没有其它用户篡改当前这条用户信息,保证线程安全,这的version就类似于StampedLock中获取锁时返回的stamp值。

StampedLock 使用注意事项

  • 从StampedLock命名就可以和ReadWriteLock的具体实现ReentrantReadWriteLock做区分,它不是可重入锁。

  • StampedLock的写锁和悲观读锁都是不支持条件变量的。

  • StampedLock如果有线程阻塞在写锁writeLock或者readLock上,这时调用中断方法interrupt()来中断阻塞的线程,那么就会造成该线程所在的CPU飙升,参考代码如下。

    线程T1获取写锁后调用LockSupport.park永久阻塞,这时线程T2去获取读锁也会阻塞等待,如果调用T2的线程中断方法,CPU将会飙升,因为等待逻辑在一个循环中,里面有一个LockSupport.pack来实现等待,满足条件才跳出循环,结束等待如果线程处于中断状态

     public class Test {
         public static void main(String[] args) throws InterruptedException {
             final StampedLock lock
                     = new StampedLock();
             Thread T1 = new Thread(()->{
                 // 获取写锁
                 lock.writeLock();
                 // 永远阻塞在此处,不释放写锁 从运行状态转为等待状态
                 LockSupport.park();
             },"T1");
             T1.start();
             // 保证T1获取写锁
             Thread.sleep(100);
             Thread T2 = new Thread(()->{
                 //阻塞在悲观读锁
                 lock.readLock();
             },"T2");
             T2.start();
             // 保证T2阻塞在读锁
             Thread.sleep(100);
             // 中断线程T2 会导致线程T2所在CPU飙升
             T2.interrupt();
             T2.join();
         }
     }
    

    所以在使用StampedLock时中断不要使用interrupt,一定要使用可中断的悲观读锁 readLockInterruptibly() 和写锁 writeLockInterruptibly()

\