JUC工具(3):StampedLock的基础和原理

什么是StampedLock?

StampedLock是在Java 8中引入的一种高性能读写锁 ,它通过引入stamp,提供了比ReentrantReadWriteLock更加灵活、更细粒度的并发控制机制,特别适合读多写少的场景,并且它没有使用AQS

StampedLock是一种基于stamp的锁机制,支持三种访问模式:

模式 说明
写锁(WriteLock) 独占锁,类似 ReentrantReadWriteLock.WriteLock
悲观读锁(ReadLock) 共享锁,类似 ReentrantReadWriteLock.ReadLock
乐观读(Optimistic Read) 无锁读取,不阻塞写线程

它在每次加锁都会返回一个long类型的stamp,解锁的时候必须使用stamp

为什么要使用StampedLock

对于传统的读写锁ReentrantReadWriteLock,它存在两个问题:

  1. 读锁饥饿写锁 :如果读线程非常多,那么会导致写线程可能长时间拿不到锁 2.读锁也是重量级锁 :即便是读,也需要CAS+队列排队,仍然有开销

为了解决这些问题,StampedLock的核心思想是:能用无锁就不用加锁

尤其是引入了乐观锁,在读多写极少的情况下性能极佳。

StampedLock三种锁模式

写锁writeLock

这是一个独占锁,同一时刻只有一个线程可以获取,其他请求写锁和读锁的线程必须等待,类似于ReentrantReadWriteLock的写锁,但是一个重要的不同之处是:StampedLock的写锁是不可重入锁。

请求该锁成功后会返回一个stamp变量用来表示该锁的版本 ,当释放该锁时需要调用unlockWrite方法并传递获取锁时的stamp参数。并且它提供了非阻塞的tryWriteLock方法。

悲观读锁readLock

这是一个共享锁,在没有线程持有写锁的时候,多个线程可以同时持有读锁,如果已经有线程持有写锁,则其他线程请求获取该读锁会被阻塞,类似于ReentrantReadWriteLock的读锁,但是一个重要的不同之处是:StampedLock的读锁是不可重入锁。

请求该锁成功后会返回一个stamp变量用来表示该锁的版本 ,当释放该锁时需要调用unlockRead方法并传递stamp参数。并且它提供了非阻塞的tryReadLock方法。

乐观读锁tryOptimisticRead

相对于悲观读锁,在操作数据前并没有通过CAS设置锁的状态,仅仅通过位运算来测试。

如果当前没有线程持有写锁,则简单地返回一个非0的stamp版本信息,获取了stamp后在具体操作数据前还需要调用validate方法验证该stamp是否已经不可用。如果在这期间,有线程获取了写锁,那么validate会返回0。

乐观读锁不需要设置锁的状态,因此也不需要释放锁,效率会高很多,但也因此在保证数据一致性上需要复制一份要操作的变量到方法栈,并且操作数据的时候,可能其他写线程已经修改了数据,而我们操作的是方法栈中的数据,也就是原本数据的一个快照。

三种锁相互转换

StampedLock还支持这三种在一定条件下进行相互转换。tryConvertToWriteLock(long stamp)期望把stamp标示的锁升级为写锁,这个函数会在以下的情况下返回一个有效的stamp代表晋升写锁成功:

  1. 当前锁已经是写锁
  2. 当前锁处于读锁,并且没有其他线程持有读锁
  3. 当前锁是乐观读锁,并且当前写锁可用

StampedLock使用示例

java 复制代码
class Point {
    // 成员变量
    private double x, y;

    // 锁实例
    private final StampedLock sl = new StampedLock();

    // 排它锁------写锁(writeLock)
    void move(double deltaX, double deltaY) {
        long stamp = sl.writeLock();
        try {
            x += deltaX;
            y += deltaY;
        } finally {
            sl.unlockWrite(stamp);
        }
    }

    // 乐观读锁(tryOptimisticRead)
    double distanceFromOrigin() {
        // (1) 尝试获取乐观读锁
        long stamp = sl.tryOptimisticRead();

        // (2) 将全部变量复制到方法体域内
        double currentX = x, currentY = y;

        // (3) 检查在(1)处获取了读锁戳记后,锁有没有被其他写线程排他性抢占
        if (!sl.validate(stamp)) {
            // (4) 如果被抢占则获取一个共享读锁(悲观获取)
            stamp = sl.readLock();
            try {
                // (5) 将全部变量复制到方法体域内
                currentX = x;
                currentY = y;
            } finally {
                // (6) 释放共享读锁
                sl.unlockRead(stamp);
            }
        }
        // (7) 返回计算结果
        return Math.sqrt(currentX * currentX + currentY * currentY);
    }

    // 使用悲观锁获取读锁,并尝试转换为写锁
    void moveIfAtOrigin(double newX, double newY) {
        // (1) 这里可以使用乐观读锁替换
        long stamp = sl.readLock();
        try {
            // (2) 如果当前点在原点则移动
            while (x == 0.0 && y == 0.0) {
                // (3) 尝试将获取的读锁升级为写锁
                long ws = sl.tryConvertToWriteLock(stamp);

                // (4) 升级成功,则更新戳记,并设置坐标值,然后退出循环
                if (ws != 0L) {
                    stamp = ws;
                    x = newX;
                    y = newY;
                    break;
                } else {
                    // (5) 读锁升级写锁失败则释放读锁,显式获取独占写锁,然后循环重试
                    sl.unlockRead(stamp);
                    stamp = sl.writeLock();
                }
            }
        } finally {
            // (6) 释放锁
            sl.unlock(stamp);
        }
    }
}
相关推荐
呱牛do it1 小时前
企业级门户网站设计与实现:基于SpringBoot + Vue3的全栈解决方案(Day 7)
java·vue
NE_STOP1 小时前
Redis--SDS字符串与集合的底层实现原理
java
直奔標竿2 小时前
Java开发者AI转型第二十二课!Spring AI 个人知识库实战(一)——架构搭建与核心契约落地
java·人工智能·后端·spring·架构
身如柳絮随风扬2 小时前
深入理解Java IO与NIO的区别:从BIO到NIO的演进
java·nio
清汤饺子2 小时前
【译】我的 AI 进阶之路:从怀疑到深度整合
前端·javascript·后端
A-Jie-Y2 小时前
JAVA设计模式-抽象工厂模式
java·设计模式
@insist1232 小时前
信息安全工程师-密码学专题(下):构建可信网络空间的核心机制
java·大数据·密码学·软考·信息安全工程师·软件水平考试
无厚2 小时前
Spring Boot中LLM流式交互的核心原理
后端·设计
摇滚侠2 小时前
Java 零基础全套视频教程,面向对象(高级),笔记 105-120
java·开发语言·笔记