Java中的StampedLock实现原理详解

 更新时间:2024年01月13日 09:59:05   作者:java架构师-太阳  
这篇文章主要介绍了Java中的StampedLock实现原理详解,ReentrantReadWriteLock采用悲观读,第一个读线程拿到锁后,第二个/第三个读线程可以拿到锁,特别是在读线程很多,写线程很少时,需要的朋友可以参考下

StampedLock(邮戳锁/版本锁/票据锁)

jdk8引入 读读不互斥,读写不互斥,写写互斥

  • ReentrantReadWriteLock采用悲观读,第一个读线程拿到锁后,第二个/第三个读线程可以拿到锁,特别是在读线程很多,写线程很少时,写线程可能一直拿不到锁,在非公平锁的情况下,导致写线程饿死
  • StampedLock引入乐观读,读时不加读锁,写锁也可进行写,避免写线程被饿死,读数据时读出来发现数据被修改了,再升级为悲观读,再读一次  
  • 所有获取锁的方法,都返回一个邮戳(Stamp),为0表示获取失败,其余值都表示获取成功
  • 所有释放锁的方法,都需要一个邮戳(Stamp),必须和成功获取锁时的一致

stamp是long类型,代表锁的状态,当归0时,表示线程获取锁失败,当释放锁或转换锁时,都要传入最初的stamp值

不可重入,如果一个线程已经获取写锁,再去获取写锁的话容易死锁

三种模式

  1. read(读悲观模式): 功能和ReentrantReadWriteLock的读锁类似,在读时不允许有写的操作
  2. write(悲观写模式): 功能和ReentrantReadWriteLock的写锁功能类似
  3. Optimistic(乐观读模式):  乐观锁,读时可有写操作,读完后检验版本号是否被写改了,若改了就再悲观读一次

代码演示

class Point {
    private double x, y;
    private final StampedLock sl = new StampedLock();

    // 多个线程调用该方法,修改x和y的值
    void move(double deltaX, double deltaY) {
       long stamp = sl.writeLock();    // 获取写锁
       try {
          x += deltaX;
          y += deltaY;
       } finally {
          sl.unlockWrite(stamp);       // 释放写锁
       }
   }

   // 多个线程调用该方法,求距离
   double distenceFromOrigin() {

       // 乐观读
       long stamp = sl.tryOptimisticRead();
       // 将共享变量拷贝到线程栈, 读:将一份数据拷贝到线程的栈内存中

       double currentX = x, currentY = y;
       // 读期间有其他线程修改数据, 读取后,对比读之前的版本号和当前的版本号,判断数据是否可用
            // 根据stamp判断在读取数据和使用数据期间,有没有其他线程修改数据
       if (!sl.validate(stamp)) {
          // 读到的是脏数据,丢弃.重新使用悲观读
          stamp = sl.readLock();
          try {
             currentX = x;
             currentY = y;
          } finally {
             sl.unlockRead(stamp);
          }
       }
       return Math.sqrt(currentX * currentX + currentY * currentY);
    }
}

如上代码,有一个Point类,多个线程调用move()方法,修改坐标;还有多个线程调用distanceFromOrigin()方法,求距离 首先,执行move操作时,要加写锁,和ReadWriteLock的用法没有区别,写操作和写操作也是互斥的 在读时,用乐观读sl.tryOptimisticRead(),相当于在读之前给数据的状态做一个快照,把数据拷贝到内存里面,在用之前,再比对一次版本号,如果版本号变了,则说明在读的期间有其他线程修改了数据,读出来的数据废弃,重新悲观读获取数据

要说明的是,这三行关键代码对顺序非常敏感,不能有重排序。因为state变量已经是volatile,所以可以禁止重排序,但stamp并不是volatile的。为此,在validate(stamp)方法里面插入内存屏障

public boolean validate(long stamp) {
      VarHandle.acquireFence();
      return (stamp & SBITS) == (state & SBITS);
    }
public class StampedLockDemo {
    static int number = 37;
    static StampedLock stampedLock = new StampedLock();
    public void write() {
        long stamp = stampedLock.writeLock();
        System.out.println(Thread.currentThread().getName()+"\t"+"写线程准备修改");
        try {
            number = number + 13;
        }finally {
            stampedLock.unlockWrite(stamp);
        }
        System.out.println(Thread.currentThread().getName()+"\t"+"写线程结束修改");
    }
    //悲观读,读没有完成时候写锁无法获得锁
    public void read() {
        long stamp = stampedLock.readLock();
        System.out.println(Thread.currentThread().getName()+"\t"+" come in readlock code block,4 seconds continue...");
        for (int i = 0; i < 4; i++) {
            // 暂停几秒钟线程
            try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
            System.out.println(Thread.currentThread().getName()+"\t"+" 正在读取中......");
        }
        try {
            int result = number;
            System.out.println(Thread.currentThread().getName()+"\t"+" 获得成员变量值result:"+result);
            System.out.println("写线程没有修改成功,读锁时候写锁无法介入,传统的读写互斥");
        }finally {
            stampedLock.unlockRead(stamp);
        }
    }
    // 乐观读,读的过程中也允许获取写锁介入
    public void tryOptimisticRead() {
        long stamp = stampedLock.tryOptimisticRead();
        int result = number;
        // 故意间隔4秒钟,很乐观认为读取中没有其它线程修改过number值,具体靠判断
        System.out.println("4秒前stampedLock.validate方法值(true无修改,false有修改)"+"\t"+stampedLock.validate(stamp));
        for (int i = 0; i < 4; i++) {
            try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
            System.out.println(Thread.currentThread().getName()+"\t"+"正在读取... "+i+" 秒" +
                    "后stampedLock.validate方法值(true无修改,false有修改)"+"\t"+stampedLock.validate(stamp));
        }
        if(!stampedLock.validate(stamp)) {
            System.out.println("有人修改过------有写操作");
            stamp = stampedLock.readLock();
            try {
                System.out.println("从乐观读 升级为 悲观读");
                result = number;
                System.out.println("重新悲观读后result:"+result);
            }finally {
                stampedLock.unlockRead(stamp);
            }
        }
        System.out.println(Thread.currentThread().getName()+"\t"+" finally value: "+result);
    }
    public static void main(String[] args) {
        StampedLockDemo resource = new StampedLockDemo();
        /* 传统版
        new Thread(() -> {
            resource.read();
        },"readThread").start();
        //暂停几秒钟线程
        try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
        new Thread(() -> {
            System.out.println(Thread.currentThread().getName()+"\t"+"----come in");
            resource.write();
        },"writeThread").start();
        //暂停几秒钟线程
        try { TimeUnit.SECONDS.sleep(4); } catch (InterruptedException e) { e.printStackTrace(); }
        System.out.println(Thread.currentThread().getName()+"\t"+"number:" +number);*/
        new Thread(() -> {
            resource.tryOptimisticRead();
        },"readThread").start();
        // 暂停2秒钟线程,读过程可以写介入,演示
        try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); }
        // 暂停6秒钟线程
        // try { TimeUnit.SECONDS.sleep(6); } catch (InterruptedException e) { e.printStackTrace(); }
        new Thread(() -> {
            System.out.println(Thread.currentThread().getName()+"\t"+"----come in");
            resource.write();
        },"writeThread").start();
    }
}

乐观读实现原理

StampedLock是一个读写锁,因此也会像读写锁那样,把一个state变量分成两半,分别表示读锁和写锁的状态。

同时,还需要一个数据的version,但是,一次CAS没有办法操作两个变量,所以这个state变量本身同时也表示了数据的version。

下面先分析state变量

public class StampedLock implements java.io.Serializable {
       private static final int LG_READERS = 7;
       private static final long RUNIT = 1L;
       private static final long WBIT = 1L << LG_READERS; // 第8位表示写锁
       private static final long RBITS = WBIT - 1L; // 最低的7位表示读锁
       private static final long RFULL = RBITS - 1L; // 读锁的数目
       private static final long ABITS = RBITS | WBIT; // 读锁和写锁状态合二为一
       private static final long SBITS = ~RBITS;
       private static final long ORIGIN = WBIT << 1; // state的初始值
       private transient volatile long state;
    }

用最低的8位表示读和写的状态,其中第8位表示写锁的状态,最低的7位表示读锁的状态。

因为写锁只有一个bit位,所以写锁是不可重入的

初始值不为0,而是把WBIT 向左移动了一位,也就是上面的ORIGIN 常量,构造方法如下所示

为什么state的初始值不设为0呢?

看乐观锁的实现:

上面两个方法必须结合起来看:当state&WBIT != 0的时候,说明有线程持有写锁,上面的tryOptimisticRead会永远返回0。

这样,再调用validate(stamp),也就是validate(0)也会永远返回false。这正是我们想要的逻辑:当有线程持有写锁的时候,validate永远返回false,无论写线程是否释放了写锁。

因为无论是否释放了(state回到初始值)写锁,state值都不为0,所以validate(0)永远为false

为什么上面的validate(…)方法不直接比较stamp=state,而要比较state&SBITS=state&SBITS 呢?

因为读锁和读锁是不互斥的! 所以,即使在“乐观读”的时候,state 值被修改了,但如果它改的是第7位,validate(…)还是会返回true。

另外要说明的一点是,上面使用了内存屏障VarHandle.acquireFence();,是因为在这行代码的下一行里面的stamp、SBITS变量不是volatile的,由此可以禁止其和前面的currentX=X,currentY=Y进行重排序 通过上面的分析,可以发现state的设计非常巧妙。

只通过一个变量,既实现了读锁、写锁的状态记录,还实现了数据的版本号的记录

到此这篇关于Java中的StampedLock实现原理详解的文章就介绍到这了,更多相关StampedLock实现原理内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • SpringBoot集成Spring security JWT实现接口权限认证

    SpringBoot集成Spring security JWT实现接口权限认证

    这篇文章主要介绍了SpringBoot集成Spring security JWT实现接口权限认证,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2021-04-04
  • 心动吗?正大光明的免费使用IntelliJ IDEA商业版

    心动吗?正大光明的免费使用IntelliJ IDEA商业版

    这篇文章主要介绍了正大光明的免费使用IntelliJ IDEA商业版,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2020-02-02
  • springBoot跨域注解@CrossOrigin用法

    springBoot跨域注解@CrossOrigin用法

    SpringFramework 4.2GA为CORS提供了支持,Spring MVC版本必须在4.2或以上才能使用@CrossOrigin注解,如果在Controller中使用@CrossOrigin后仍遇到跨域问题,可能是因为@RequestMapping中未指定HTTP方法,指定具体的Get或Post方法后,可以解决这一问题
    2024-10-10
  • Java中的BigDecimal使用问题小结

    Java中的BigDecimal使用问题小结

    BigDecimal 是 Java 中用于进行精确十进制计算的重要类, 它适用于金融、科学等需要高精度计算的场景,这篇文章让我来详细讲解 Java 中的 BigDecimal 类,包括它的用途、适用场景、以及潜在问题,感兴趣的朋友一起看看吧
    2025-04-04
  • Java C++题解leetcode 1684统计一致字符串的数目示例

    Java C++题解leetcode 1684统计一致字符串的数目示例

    这篇文章主要为大家介绍了Java C++题解leetcode 1684统计一致字符串的数目示例详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2023-01-01
  • 使用Springboot实现OAuth服务的示例详解

    使用Springboot实现OAuth服务的示例详解

    OAuth(Open Authorization)是一个开放标准,用于授权第三方应用程序访问用户资源,而不需要共享用户凭证。本文主要介绍了如何使用Springboot实现一个OAuth服务,需要的可以参考一下
    2023-05-05
  • idea常用习惯操作设置方法图解

    idea常用习惯操作设置方法图解

    这篇文章主要介绍了idea常用习惯操作设置方法,本文通过图文并茂的形式给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2021-03-03
  • 使用SpringDataJpa创建中间表

    使用SpringDataJpa创建中间表

    这篇文章主要介绍了使用SpringDataJpa创建中间表,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2022-06-06
  • JUC中Future及CompletableFuture的用法与说明

    JUC中Future及CompletableFuture的用法与说明

    本文介绍了Future接口及其实现类FutureTask的使用方法,指出了其阻塞和CPU空转问题,为了解决这些问题,介绍了CompletableFuture的异步任务编排,可以自动回调异步任务结束时的方法,解决了阻塞和轮询问题,同时提供了链式调用、函数式编程等等提高编程效率
    2026-04-04
  • Java服务限流算法的6种实现

    Java服务限流算法的6种实现

    服务限流是指通过控制请求的速率或次数来达到保护服务的目的,本文主要介绍了Java服务限流算法的6种实现,具有一定的参考价值,感兴趣的可以了解一下
    2023-05-05

最新评论