阿里云-云小站(无限量代金券发放中)
【腾讯云】云服务器、云数据库、COS、CDN、短信等热卖云产品特惠抢购

2023年一起来认识一下StampedLock吧

62次阅读
没有评论

共计 4373 个字符,预计需要花费 11 分钟才能阅读完成。

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

想到读写锁,大家第一时间想到的可能是 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():如果持有写锁,则释放该锁,而不需要戳值。这种方法可能对错误后的恢复很有用。
  • long stamp = lock.writeLock();
    try {....} finally {lock.unlockWrite(stamp);
    }
    读模式

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

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

    乐观读也就是若读的操作很多,写的操作很少的情况下,你可以乐观地认为,写入与读取同时发生几率很少,因此不悲观地使用完全的读取锁定,程序可以查看读取资料之后,是否遭到写入执行的变更,再采取后续的措施 (重新读取变更信息,或者抛出异常),这一个小小改进,可大幅度提高程序的吞吐量。

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

  • tryOptimisticRead(): 返回稍后可以验证的戳记,如果独占锁定则返回零。
  • boolean validate(long stamp): 如果自给定戳记发行以来锁还没有被独占获取,则返回 true。
  • long stamp = lock.tryOptimisticRead();
    // 验戳
    if(!lock.validate(stamp)){// 锁升级}

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

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

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

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

    演示例子

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

    @Slf4j
    @Data
    public class Point {
        private double x, y;
        private final StampedLock sl = new StampedLock();
    
        void move(double deltaX, double throws{
            // 涉及对共享资源的修改,使用写锁 - 独占操作
            long stamp = sl.writeLock();
            log.info("writeLock lock success");
            Thread.sleep(500);
            try {
                x += deltaX;
                y += deltaY;
            } finally {sl.unlockWrite(stamp);
                log.info("unlock write lock success");
            }
        }
    
        /**
         * 使用乐观读锁访问共享资源
         * 注意:乐观读锁在保证数据一致性上需要拷贝一份要操作的变量到方法栈,并且在操作数据时候可能其他写线程已经修改了数据,* 而我们操作的是方法栈里面的数据,也就是一个快照,所以最多返回的不是最新的数据,但是一致性还是得到保障的。*
         * @return
         */
        double distanceFromOrigin() throws{long stamp = sl.tryOptimisticRead();    // 使用乐观读锁
            log.info("tryOptimisticRead lock success");
            // 睡一秒中
            Thread.sleep(1000);
            double currentX = x, currentY = y;      // 拷贝共享资源到本地方法栈中
            if (!sl.validate(stamp)) {              // 如果有写锁被占用,可能造成数据不一致,所以要切换到普通读锁模式
                log.info("validate stamp error");
                stamp = sl.readLock();
                log.info("readLock success");
                try {
                    currentX = x;
                    currentY = y;
                } finally {sl.unlockRead(stamp);
                    log.info("unlock read success");
                }
            }
            return Math.sqrt(currentX * currentX + currentY * currentY);
        }
    
        void moveIfAtOrigin(double newX, double{ // 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);
            }
        }
    }

    测试用例:

    @Test
    public void testStamped() throws InterruptedException {Point point = new Point();
        point.setX(1);
        point.setY(2);
        // 线程 0 执行了乐观读
        Thread thread0 = new Thread(() -> {
            try {
                // 乐观读
                point.distanceFromOrigin();} catch (InterruptedException e) {e.printStackTrace();
            }
        }, "thread-0");
        thread0.start();
    
        Thread.sleep(500);
        // 线程 1 执行写锁
        Thread thread1 = new Thread(() -> {
            // 乐观读
            try {point.move(3, 4);
            } catch (InterruptedException e) {e.printStackTrace();
            }
        }, "thread-1");
        thread1.start();
    
        thread0.join();
        thread1.join();}

    结果:

    2023 年一起来认识一下 StampedLock 吧

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

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

    2023 年一起来认识一下 StampedLock 吧

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

    2023 年一起来认识一下 StampedLock 吧

    下图是吞吐量提高:

    2023 年一起来认识一下 StampedLock 吧

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

  • 不支持条件变量 Condition
  • 不支持可重入
  • 所以最终选择 StampedLock​还是 ReentrantReadWriteLock,还是要看具体的业务场景。

    总结

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

    阿里云 2 核 2G 服务器 3M 带宽 61 元 1 年,有高配

    腾讯云新客低至 82 元 / 年,老客户 99 元 / 年

    代金券:在阿里云专用满减优惠券

    正文完
    星哥玩云-微信公众号
    post-qrcode
     0
    星锅
    版权声明:本站原创文章,由 星锅 于2024-07-24发表,共计4373字。
    转载说明:除特殊说明外本站文章皆由CC-4.0协议发布,转载请注明出处。
    【腾讯云】推广者专属福利,新客户无门槛领取总价值高达2860元代金券,每种代金券限量500张,先到先得。
    阿里云-最新活动爆款每日限量供应
    评论(没有评论)
    验证码
    【腾讯云】云服务器、云数据库、COS、CDN、短信等云产品特惠热卖中