美文网首页
StampedLock

StampedLock

作者: 我可能是个假开发 | 来源:发表于2023-02-23 23:30 被阅读0次

    在读多写少的场景中,Java 在 1.8 这个版本里,提供了一种叫 StampedLock 的锁,它的性能比读写锁还要好。

    一、StampedLock 支持的三种锁模式

    • 写锁
    • 悲观读锁
    • 乐观读

    写锁、悲观读锁的语义和 ReadWriteLock 的写锁、读锁的语义非常类似,允许多个线程同时获取悲观读锁,但是只允许一个线程获取写锁,写锁和悲观读锁是互斥的。不同的是:StampedLock 里的写锁和悲观读锁加锁成功之后,都会返回一个 stamp;然后解锁的时候,需要传入这个 stamp。

    final StampedLock sl = new StampedLock();
    
    // 获取/释放悲观读锁示意代码
    long stamp = sl.readLock();
    try {
      //省略业务相关代码
    } finally {
      sl.unlockRead(stamp);
    }
    
    // 获取/释放写锁示意代码
    long stamp = sl.writeLock();
    try {
      //省略业务相关代码
    } finally {
      sl.unlockWrite(stamp);
    }
    

    StampedLock 的性能之所以比 ReadWriteLock 还要好,其关键是 StampedLock 支持乐观读的方式。ReadWriteLock 支持多个线程同时读,但是当多个线程同时读的时候,所有的写操作会被阻塞;而 StampedLock 提供的乐观读,是允许一个线程获取写锁的,也就是说不是所有的写操作都被阻塞。
    乐观读这个操作是无锁的,所以相比较 ReadWriteLock 的读锁,乐观读的性能更好一些。

    class Point {
      private int x, y;
      final StampedLock sl = new StampedLock();
      //计算到原点的距离  
      int 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);
          }
        }
        return Math.sqrt(
          curX * curX + curY * curY);
      }
    }
    

    在 distanceFromOrigin() 这个方法中,首先通过调用 tryOptimisticRead() 获取了一个 stamp,这里的 tryOptimisticRead() 就是乐观读。之后将共享变量 x 和 y 读入方法的局部变量中,不过需要注意的是,由于 tryOptimisticRead() 是无锁的,所以共享变量 x 和 y 读入方法局部变量时,x 和 y 有可能被其他线程修改了。因此最后读完之后,还需要再次验证一下是否存在写操作,这个验证操作是通过调用 validate(stamp) 来实现的。
    如果执行乐观读操作的期间,存在写操作,会把乐观读升级为悲观读锁,否则就需要在一个循环里反复执行乐观读,直到执行乐观读操作的期间没有写操作(只有这样才能保证 x 和 y 的正确性和一致性),而循环读会浪费大量的 CPU。升级为悲观读锁,代码简练且不易出错。

    二、乐观读

    StampedLock 的乐观读和数据库的乐观锁有异曲同工之妙。

    乐观锁的实现很简单,在表 里增加了一个数值型版本号字段 version,每次更新表的时候,都将 version 字段加 1。
    查询的时候需要把 version 字段查出来,更新的时候要利用 version 字段做验证。这个 version 字段就类似于 StampedLock 里面的 stamp。

    三、StampedLock 使用注意事项

    • StampedLock 的功能仅仅是 ReadWriteLock 的子集。
    • StampedLock 不支持重入。
    • StampedLock 的悲观读锁、写锁都不支持条件变量

    如果线程阻塞在 StampedLock 的 readLock() 或者 writeLock() 上时,此时调用该阻塞线程的 interrupt() 方法,会导致 CPU 飙升。

    final StampedLock lock = new StampedLock();
    Thread T1 = new Thread(()->{
      // 获取写锁
      lock.writeLock();
      // 永远阻塞在此处,不释放写锁
      LockSupport.park();
    });
    T1.start();
    // 保证T1获取写锁
    Thread.sleep(100);
    Thread T2 = new Thread(()->
      //阻塞在悲观读锁
      lock.readLock()
    );
    T2.start();
    // 保证T2阻塞在读锁
    Thread.sleep(100);
    //中断线程T2
    //会导致线程T2所在CPU飙升
    T2.interrupt();
    T2.join();
    

    线程 T1 获取写锁之后将自己阻塞,线程 T2 尝试获取悲观读锁,也会阻塞;如果此时调用线程 T2 的 interrupt() 方法来中断线程 T2 的话,会导致线程 T2 所在 CPU 会飙升到 100%。

    四、StampedLock使用模板

    StampedLock 读模板:

    final StampedLock sl = new StampedLock();
    
    // 乐观读
    long stamp = sl.tryOptimisticRead();
    // 读入方法局部变量
    ......
    // 校验stamp
    if (!sl.validate(stamp)){
      // 升级为悲观读锁
      stamp = sl.readLock();
      try {
        // 读入方法局部变量
        .....
      } finally {
        //释放悲观读锁
        sl.unlockRead(stamp);
      }
    }
    //使用方法局部变量执行业务操作
    ......
    

    StampedLock 写模板:

    long stamp = sl.writeLock();
    try {
      // 写共享变量
      ......
    } finally {
      sl.unlockWrite(stamp);
    }
    

    相关文章

      网友评论

          本文标题:StampedLock

          本文链接:https://www.haomeiwen.com/subject/pfitldtx.html