• 【Java并发工具类】StampedLock:比读写锁更快的锁


    前言

    ReadWriteLock适用于读多写少的场景,允许多个线程同时读取共享变量。但在读多写少的场景中,还有更快的技术方案。在Java 1.8中, 提供了StampedLock锁,它的性能就比读写锁还要好。下面我们介绍StampedLock的使用方法、内部工作原理以及在使用过程中需要注意的事项。

    StampedLock支持的三种锁模式

    ReadWriteLock支持两种访问模式:读锁和写锁,而StampedLock支持三种访问模式:写锁、悲观读锁和乐观读。

    其中写锁和悲观读锁的语义与ReadWriteLock中的写锁和读锁语义类似,允许多个线程同时获取悲观读锁,只允许一个线程获取写锁。与ReadWriteLock不同的是,StampedLock中的写锁和悲观读锁加锁成功之后,都会返回一个stamp标记,然后解锁的时候需要传入这个stamp。

    相关示例代码如下(代码来自参考[1])

    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提供了乐观读,当有多个线程同时读共享变量允许一个线程获取写锁,也就是说不是所有写操作都会被阻塞。

    需要注意,StampedLock提供的是“乐观读”而不是“乐观读锁”,这表示乐观读是无锁的,这也是其比ReadWriteLock读锁性能好的原因。

    乐观读的使用示例(代码来自参考[1]):

    class Point{
        private int  x, y;
        final StampedLock sl = new StampedLock();
        // 计算到原点的距离
        double distanceFromOrigin() {
            long stamp = sl.tryOptimisticRead(); //乐观读
            //读取全局变量存储到局部变量中 在读入的过程中,数据可能被修改
            int curX = x;
            int curY = y;
            //判断进行读操作期间,是否存在写操作,如果存在,则sl.validate(stamp)返回false
            if(!sl.validate(stamp)) {
                stamp = sl.readLock(); //升级为悲观读锁 一切的写操作都会被阻塞
                try {
                    curX = x;
                    curY = y;      
                }finally {
                    sl.unlockRead(stamp); //释放悲观读锁
                }
            }
            return Math.sqrt(curX*curX + curY*curY);
        }
    }
    

    我们将共享变量x,y读入方法的局部变量中,因为tryOptimisticRead()是无锁的,所以,共享变量x和y读入方法局部变量时,x和y有可能被其他线程修改了。因此,最后读完之后,还需要再次验证一下在读入过程中是否存在写操作,这个验证操作是通过调用validate(stamp)来实现的。
    如果在执行乐观读操作期间,存在写操作,会把乐观读升级为悲观读锁。
    如果不使用这种做法,那么就可能需要使用循环来执行反复读,直到执行乐观读操作的期间没有写操作,但是循环会浪费大量的CPU。
    所以,升级为悲观读锁,代码简练且不易出错。

    StampedLock乐观读的理解

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

    通过下面这个例子来理解:
    在ERP的生产模块中,会有多个人通过ERP系统提供的UI同时修改同一条生产订单,那如何保证生产订单数据是并发安全的?
    一种解决方案是采用乐观锁。

    在生产订单的表product_doc里面增加了一个数据型版本号字段vresion,每次更新product_doc这个表的时候,都将version字段加1。生产订单的UI在展示的时候,需要查询数据库,此时将这个version字段和其他业务字段一起返回给生产订单UI。

    假设用户查询的生产订单的id=777,那么SQL语句类似如下:

    select id, ..., version
    from product_doc
    where id=777
    

    用户在生产订单UI执行保存操作的时候,后台利用下面的SQL语句更新生产订单,此处我们假设该条生产订单的version=4:

    update product_doc
    set version=version+1,...
    where id=777 and version=4
    

    如果这条SQL语句执行成功并且返回条数等于1,那么说明从生产订单UI执行查询操作到执行保存期间,没有其他人修改过这条数据。因为如果这期间有人修改过这条数据,那么版本号字段一定会大于4。

    数据库中的乐观锁,查询的时候,需要把version字段查出来,更新的时候要利用version字段做验证。StampedLock里面的stamp就类似于这个version字段。

    StampedLock使用注意事项

    StampedLock的功能仅仅是ReadWriteLock的子集,所以在使用时,还是需要注意一些地方:

    1. StampedLock在命名上没有增加Reentrant,所以,猜想StampedLock不支持重入。事实上,确实如此,StampedLock是不支持重入的。

    2. StampedLock的悲观读锁、写锁都不支持条件变量

    3. 如果线程阻塞在 StampedLock 的 readLock() 或者writeLock()上时,调用该阻塞线程的interrupt()方法,会导致 CPU 飙升。(代码来自参考[1])

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

      线程 T1 获取写锁之后将自己阻塞,线程 T2 尝试获取悲观读锁,也会阻塞;如果此时调用线程 T2 的 interrupt() 方法来中断线程 T2 的话,会发现线程 T2 所在 CPU 会飙升到 100%。(看专栏时明白线程T2获取悲观读锁会被阻塞,但是直到现在也不明白为什么调用T2的interrupt()方法会导致CPU飙升,望路过的看官解答。)
      替代方法便是使用悲观读锁readLockInterruptibly()和写锁writeLockInterruptibly()

    StampedLock官方示例使用读写锁模板

    精简Java官方示例后,可形成如下模板(代码来自参考[1])

    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,欲知更详细的请参考[3],无意中发现的大神博客,推荐起(•̀ᴗ•́)و ̑̑

    参考:
    [1] 极客时间专栏王宝令《Java并发编程实战》
    [2] whoshiyeguiren.数据库乐观锁和悲观锁的理解和实现(转载&总结).https://blog.csdn.net/woshiyeguiren/article/details/80277475
    [3] Ressmix.Java多线程进阶(十一)—— J.U.C之locks框架:StampedLock.https://segmentfault.com/a/1190000015808032?utm_source=tag-newest

  • 相关阅读:
    ORA-01733: virtual column not allowed here
    五年磨一剑:Java 开源博客 Solo 1.0.0 发布了!
    五年磨一剑:Java 开源博客 Solo 1.0.0 发布了!
    关于Java中equal函数和==的一些区别
    <Android Framework 之路>Android5.1 Camera Framework(四)——框架总结
    Sqlite基本命令集合(linux/fedora/ubuntu)
    简介分布式计算系统的硬件架构
    <Android Framework 之路>Android5.1 Camera Framework(三)
    “调试器的协议与调试对象不兼容”错误的解决
    log4j:WARN Please initialize the log4j system properly.解决方案
  • 原文地址:https://www.cnblogs.com/myworld7/p/12332911.html
Copyright © 2020-2023  润新知