分享

读写锁ReentrantReadWriteLock已经out了,StampedLock更能装叉

本帖最后由 阿飞 于 2023-1-3 08:20 编辑

概述
想到读写锁,大家第一时间想到的可能是ReentrantReadWriteLock​。实际上,在jdk8以后,java提供了一个性能更优越的读写锁并发类StampedLock​,该类的设计初衷是作为一个内部工具类,用于辅助开发其它线程安全组件,用得好,该类可以提升系统性能,用不好,容易产生死锁和其它莫名其妙的问题。本文主要和大家一起学习下StampedLock的功能和使用。

StampedLock介绍
StampedLock​的状态由版本和模式组成。锁获取方法返回一个戳,该戳表示并控制对锁状态的访问。StampedLock提供了3种模式控制访问锁:

写模式
获取写锁,它是独占的,当锁处于写模式时,无法获得读锁,所有乐观读验证都将失败。

  • writeLock(): 阻塞等待独占获取锁,返回一个戳, 如果是0表示获取失败。
  • tryWriteLock():尝试获取一个写锁,返回一个戳, 如果是0表示获取失败。
  • long tryWriteLock(long time, TimeUnit unit): 尝试获取一个独占写锁,可以等待一段事件,返回一个戳, 如果是0表示获取失败。
  • long writeLockInterruptibly(): 试获取一个独占写锁,可以被中断,返回一个戳, 如果是0表示获取失败。
  • unlockWrite(long stamp):释放独占写锁,传入之前获取的戳。
  • tryUnlockWrite():如果持有写锁,则释放该锁,而不需要戳值。这种方法可能对错误后的恢复很有用。



  1. long stamp = lock.writeLock();
  2. try {
  3.     ....
  4. } finally {
  5.     lock.unlockWrite(stamp);
  6. }
复制代码


读模式
悲观的方式后去非独占读锁。

readLock(): 阻塞等待获取非独占的读锁,返回一个戳, 如果是0表示获取失败。
tryReadLock():尝试获取一个读锁,返回一个戳, 如果是0表示获取失败。
long tryReadLock(long time, TimeUnit unit): 尝试获取一个读锁,可以等待一段事件,返回一个戳, 如果是0表示获取失败。
long readLockInterruptibly(): 阻塞等待获取非独占的读锁,可以被中断,返回一个戳, 如果是0表示获取失败。
unlockRead(long stamp):释放非独占的读锁,传入之前获取的戳。
tryUnlockRead():如果读锁被持有,则释放一次持有,而不需要戳值。这种方法可能对错误后的恢复很有用。

  1. long stamp = lock.readLock();
  2. try {
  3.     ....
  4. } finally {
  5.     lock.unlockRead(stamp);        
  6. }
复制代码
乐观读模式
乐观读也就是若读的操作很多,写的操作很少的情况下,你可以乐观地认为,写入与读取同时发生几率很少,因此不悲观地使用完全的读取锁定,程序可以查看读取资料之后,是否遭到写入执行的变更,再采取后续的措施(重新读取变更信息,或者抛出异常) ,这一个小小改进,可大幅度提高程序的吞吐量。

StampedLock​支持 tryOptimisticRead()方法,读取完毕后做一次戳校验,如果校验通过,表示这期间没有其他线程的写操作,数据可以安全使用,如果校验没通过,需要重新获取读锁,保证数据一致性。

tryOptimisticRead(): 返回稍后可以验证的戳记,如果独占锁定则返回零。
boolean validate(long stamp): 如果自给定戳记发行以来锁还没有被独占获取,则返回true。

  1. long stamp = lock.tryOptimisticRead();
  2. // 验戳
  3. if(!lock.validate(stamp)){
  4.   // 锁升级
  5. }
复制代码


此外,StampedLock 提供了api实现上面3种方式进行转换:

long tryConvertToWriteLock(long stamp)
如果锁状态与给定的戳记匹配,则执行以下操作之一。如果戳记表示持有写锁,则返回它。或者,如果是读锁,如果写锁可用,则释放读锁并返回写戳记。或者,如果是乐观读,则仅在立即可用时返回写戳记。该方法在所有其他情况下返回零

long tryConvertToReadLock(long stamp)
如果锁状态与给定的戳记匹配,则执行以下操作之一。如果戳记表示持有写锁,则释放它并获得读锁。或者,如果是读锁,返回它。或者,如果是乐观读,则仅在立即可用时才获得读锁并返回读戳记。该方法在所有其他情况下返回零。

long tryConvertToOptimisticRead(long stamp)
如果锁状态与给定的戳记匹配,那么如果戳记表示持有锁,则释放它并返回一个观察戳记。或者,如果是乐观读,则在验证后返回它。该方法在所有其他情况下返回0,因此作为“tryUnlock”的形式可能很有用。



演示例子
下面用一个例子演示下StampedLock的使用,例子来源jdk中的javadoc。

  1. @Slf4j
  2. @Data
  3. public class Point {
  4.     private double x, y;
  5.     private final StampedLock sl = new StampedLock();
  6.     void move(double deltaX, double throws{
  7.         //涉及对共享资源的修改,使用写锁-独占操作
  8.         long stamp = sl.writeLock();
  9.         log.info("writeLock lock success");
  10.         Thread.sleep(500);
  11.         try {
  12.             x += deltaX;
  13.             y += deltaY;
  14.         } finally {
  15.             sl.unlockWrite(stamp);
  16.             log.info("unlock write lock success");
  17.         }
  18.     }
  19.     /**
  20.      * 使用乐观读锁访问共享资源
  21.      * 注意:乐观读锁在保证数据一致性上需要拷贝一份要操作的变量到方法栈,并且在操作数据时候可能其他写线程已经修改了数据,
  22.      * 而我们操作的是方法栈里面的数据,也就是一个快照,所以最多返回的不是最新的数据,但是一致性还是得到保障的。
  23.      *
  24.      * @return
  25.      */
  26.     double distanceFromOrigin() throws{
  27.         long stamp = sl.tryOptimisticRead();    // 使用乐观读锁
  28.         log.info("tryOptimisticRead lock success");
  29.         // 睡一秒中
  30.         Thread.sleep(1000);
  31.         double currentX = x, currentY = y;      // 拷贝共享资源到本地方法栈中
  32.         if (!sl.validate(stamp)) {              // 如果有写锁被占用,可能造成数据不一致,所以要切换到普通读锁模式
  33.             log.info("validate stamp error");
  34.             stamp = sl.readLock();
  35.             log.info("readLock success");
  36.             try {
  37.                 currentX = x;
  38.                 currentY = y;
  39.             } finally {
  40.                 sl.unlockRead(stamp);
  41.                 log.info("unlock read success");
  42.             }
  43.         }
  44.         return Math.sqrt(currentX * currentX + currentY * currentY);
  45.     }
  46.     void moveIfAtOrigin(double newX, double{ // upgrade
  47.         // Could instead start with optimistic, not read mode
  48.         long stamp = sl.readLock();
  49.         try {
  50.             while (x == 0.0 && y == 0.0) {
  51.                 long ws = sl.tryConvertToWriteLock(stamp);  //读锁转换为写锁
  52.                 if (ws != 0L) {
  53.                     stamp = ws;
  54.                     x = newX;
  55.                     y = newY;
  56.                     break;
  57.                 } else {
  58.                     sl.unlockRead(stamp);
  59.                     stamp = sl.writeLock();
  60.                 }
  61.             }
  62.         } finally {
  63.             sl.unlock(stamp);
  64.         }
  65.     }
  66. }
复制代码
测试用例:
  1. @Test
  2. public void testStamped() throws InterruptedException {
  3.     Point point = new Point();
  4.     point.setX(1);
  5.     point.setY(2);
  6.     // 线程0 执行了乐观读
  7.     Thread thread0 = new Thread(() -> {
  8.         try {
  9.             // 乐观读
  10.             point.distanceFromOrigin();
  11.         } catch (InterruptedException e) {
  12.             e.printStackTrace();
  13.         }
  14.     }, "thread-0");
  15.     thread0.start();
  16.     Thread.sleep(500);
  17.     // 线程1 执行写锁
  18.     Thread thread1 = new Thread(() -> {
  19.         // 乐观读
  20.         try {
  21.             point.move(3, 4);
  22.         } catch (InterruptedException e) {
  23.             e.printStackTrace();
  24.         }
  25.     }, "thread-1");
  26.     thread1.start();
  27.     thread0.join();
  28.     thread1.join();
  29. }
复制代码
结果:



1.png


性能对比
正是由于StampedLock​的乐观读模式,早就StampedLock的高性能和高吞吐量,那么具体的性能提高有多少呢?

下图是和ReadWritLock相比,在一个线程情况下,读速度是其4倍左右,写是1倍。


1.png

下图是16个线程情况下,读性能是其几十倍,写性能也是近10倍左右:


1.png

下图是吞吐量提高:


1.png

那么这样是不是说StampedLock​可以全方位的替代ReentrantReadWriteLock​, 答案是否定的,StampedLock​相对于ReentrantReadWriteLock有下面两个问题:

  • 不支持条件变量Condition
  • 不支持可重入

所以最终选择StampedLock​还是ReentrantReadWriteLock,还是要看具体的业务场景。


总结
本文主要讲解了StampedLock​的功能和使用,至于原理,StampedLock虽然不像其它锁一样定义了内部类来实现AQS框架,但是StampedLock的基本实现思路还是利用CLH队列进行线程的管理,通过同步状态值来表示锁的状态和类型,具体的源码实现大家感兴趣的自己可以追踪看看。


每天进步一点点


没找到任何评论,期待你打破沉寂

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

关闭

推荐上一条 /2 下一条