比读写锁更快的 StampedLock

简介: 比读写锁更快的 StampedLock

预计阅读所需时间 7 分钟,建议收藏

我们先回顾上一篇 ReentrantReadWriteLock 读写锁,为什么有了 ReentrantReadWriteLock,还要引入 StampLock

ReentrantReadWriteLock 使得多个读线程同时持有读锁(只要写未被占用),而写锁是独占的。但是很容易造成 “饥饿问题”:

读线程非常多,写线程很少的情况下,很容易导致写线程 “饥饿”

StampedLock 支持的三种锁模式

我们先来看看在使用上StampedLock 和上一篇文章讲的 ReadWriteLock 有哪些区别。

  1. ReadWriteLock 支持两种模式:一种是读锁,一种是写锁。写锁独占,读读共享、读写互斥。
  2. StampedLock 支持三种模式,分别是:写锁悲观读锁乐观读。其中,写锁、悲观读锁的语义和 ReadWriteLock 的写锁、读锁的语义非常类似,允许多个线程同时获取悲观读锁,但是只允许一个线程获取写锁,写锁和悲观读锁是互斥的。不同的是:StampedLock 里的写锁和悲观读锁加锁成功之后,都会返回一个 stamp;然后解锁的时候,需要传入这个 stamp。

StampedLock 支持读锁和写锁的相互转换 我们知道 RRW 中,当线程获取到写锁后,可以降级为读锁,但是读锁是不能直接升级为写锁的。StampedLock 提供了读锁和写锁相互转换的功能,使得该类支持更多的应用场景。

之所以性能比 ReentrantReadWriteLock好,其关键就是支持乐观读。ReadWriteLock 支持多个线程同时读,但是当多个线程同时读的时候,所有的写操作会被阻塞;

**而 StampedLock 提供的乐观读,是允许一个线程获取写锁的,也就是说不是所有的写操作都被阻塞。**

注意这里是乐观读,并不是 “乐观读锁”,其实它是无锁的,其实它跟数据库的乐观锁有异曲同工之妙。

乐观锁的实现很简单,在生产订单的表 product_doc 里增加了一个数值型版本号字段 version,每次更新 product_doc 这个表的时候,都将 version 字段加 1。

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

而更新的时候匹配 version 才更新。

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

你会发现数据库里的乐观锁,查询的时候需要把 version 字段查出来,更新的时候要利用 version 字段做验证。这个 version 字段就类似于 StampedLock 里面的 stamp。这样对比着看,相信你会更容易理解 StampedLock 里乐观读的用法。

StampedLock 代码示例

class Point {
    // 共享变量 x、y 坐标
    private double x, y;
    private final StampedLock sl = new StampedLock();
    /**
     * 移动坐标
     *
     * @param deltaX
     * @param deltaY
     */
    public void move(double deltaX, double deltaY) {
        long stamp = sl.writeLock(); //涉及到对共享资源的修改,使用写锁-独占
        try {
            x += deltaX;
            y += deltaY;
        } finally {
            sl.unlockWrite(stamp);
        }
    }
    /**
     * 使用乐观读访问共享资源:计算到原点的距离。
     *  注意:乐观读锁在保证数据一致性上需要拷贝一份要操作的变量到方法栈,并且在操作数据时候      * 可能其他写线程已经修改了数据,
     * 而我们操作的是方法栈里面的数据,也就是一个快照,所以最多返回的不是最新的数据,但是一致性还是得到保障的。
     *
     * @return
     */
    public double distanceFromOrigin() {
        //乐观读
        long stamp = sl.tryOptimisticRead();
        // 读取共享数据到局部变量
        double currentX = x, currentY = y;
        //读操作期间是否存在写操作,若存在则升级为悲观读锁,并重新读取共享变量到局部变量
        if (!sl.validate(stamp)) {
            stamp = sl.readLock();
            try {
                currentX = x;
                currentY = y;
            } finally {
                //释放悲观读
                sl.unlockRead(stamp);
            }
        }
        return Math.sqrt(currentX * currentX + currentY * currentY);
    }
    /**
     * 读锁转换写锁:若当前坐标在原点则移动
     *
     * @param newX
     * @param newY
     */
    public void moveIfAtOrigin(double newX, double newY) {
        // 不能直接使用乐观读,不是只读的方法
        long stamp = sl.readLock();
        try {
            while (x == 0.0 && y == 0.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);
        }
    }
}

上述例子中,特殊的就是 distanceFromOrigin()moveIfAtOrigin() 方法,第一个方法使用了 乐观读,让读写可以并发执行,通过上面例子我们也总结出 乐观读的使用模板。第二个则是使用了读锁转换成写锁的方式。

long stamp = lock.tryOptimisticRead();  // 乐观读
copyVaraibale2ThreadMemory();           // 拷贝变量到线程本地堆栈
if(!lock.validate(stamp)){              // 校验是否被修改
    long stamp = lock.readLock();       // 获取悲观读锁
    try {
        copyVaraibale2ThreadMemory();   // 拷贝变量到线程本地堆栈
     } finally {
       lock.unlock(stamp);              // 释放悲观锁
    }
}
useThreadMemoryVarables();             // 使用局部变量进行数据操作

StampedLock 使用注意事项

对于读多写少的场景 StampedLock 性能很好,简单的应用场景基本上可以替代 ReadWriteLock,但是StampedLock 的功能仅仅是 ReadWriteLock 的子集,在使用的时候,还是有几个地方需要注意一下。

  1. StampedLock 在命名上并没有增加 Reentrant,想必你已经猜测到 StampedLock 应该是不可重入的。事实上,的确是这样的,StampedLock 不支持重入。这个是在使用中必须要特别注意的。
  2. 另外,StampedLock 的悲观读锁、写锁都不支持条件变量,这个也需要注意 。
  3. 如果线程阻塞在 StampedLock 的 readLock() 或者 writeLock() 上时,此时调用该阻塞线程的 interrupt() 方法,会导致 CPU 飙升。所以,使用 StampedLock 一定不要调用中断操作,如果需要支持中断功能,一定使用可中断的悲观读锁 readLockInterruptibly() 和写锁 writeLockInterruptibly()。这个规则一定要记清楚。


相关文章
|
5月前
|
人工智能 自然语言处理 安全
基于LlamaIndex实现CodeAct Agent:代码执行工作流的技术架构与原理
CodeAct是一种先进的AI辅助系统范式,深度融合自然语言处理与代码执行能力。通过自定义代码执行代理,开发者可精准控制代码生成、执行及管理流程。本文基于LlamaIndex框架构建CodeAct Agent,解析其技术架构,包括代码执行环境、工作流定义系统、提示工程机制和状态管理系统。同时探讨安全性考量及应用场景,如软件开发、数据科学和教育领域。未来发展方向涵盖更精细的代码生成、多语言支持及更强的安全隔离机制,推动AI辅助编程边界拓展。
218 3
基于LlamaIndex实现CodeAct Agent:代码执行工作流的技术架构与原理
|
SQL JSON 大数据
ElasticSearch的简单介绍与使用【进阶检索】 实时搜索 | 分布式搜索 | 全文搜索 | 大数据处理 | 搜索过滤 | 搜索排序
这篇文章是Elasticsearch的进阶使用指南,涵盖了Search API的两种检索方式、Query DSL的基本语法和多种查询示例,包括全文检索、短语匹配、多字段匹配、复合查询、结果过滤、聚合操作以及Mapping的概念和操作,还讨论了Elasticsearch 7.x和8.x版本中type概念的变更和数据迁移的方法。
ElasticSearch的简单介绍与使用【进阶检索】 实时搜索 | 分布式搜索 | 全文搜索 | 大数据处理 | 搜索过滤 | 搜索排序
|
6月前
|
JavaScript 前端开发
开发和内网部署正常,反向代理后出现404和图片加载失败的解决方案;部署到公网后报错404;部署到公网后图片加载出错;动态渲染获取图片失败
博客不应该只有代码和解决方案,重点应该在于给出解决方案的同时分享思维模式,只有思维才能可持续地解决问题,只有思维才是真正值得学习和分享的核心要素。如果这篇博客能给您带来一点帮助,麻烦您点个赞支持一下,还可以收藏起来以备不时之需,有疑问和错误欢迎在评论区指出~
|
7月前
|
机器学习/深度学习 人工智能 算法
DeepSeek-R1论文细节时间线梳理
中国AI初创公司DeepSeek发布了大语言模型R1,该模型在推理任务上媲美OpenAI的ChatGPT,且训练成本仅600万美元。DeepSeek由杭州对冲基金High-Flyer支持,总部位于杭州和北京。R1基于V3-Base,使用监督微调和强化学习训练,针对硬件限制进行了优化。模型在多语言处理、推理风格等方面表现出色,但存在一些局限性,如法语表现欠佳、偶尔切换语言等。DeepSeek的创新技术包括FP8量化、多头潜在注意力和蒸馏方法,引发了广泛关注和讨论。开源社区正积极尝试复现其结果,但面临训练数据和代码未公开的挑战。DeepSeek的低成本高效训练策略为AI领域带来了新的思考方向。
442 2
|
自然语言处理 算法 搜索推荐
|
9月前
|
存储 Prometheus 监控
Docker容器内进行应用调试与故障排除的方法与技巧,包括使用日志、进入容器检查、利用监控工具及检查配置等,旨在帮助用户有效应对应用部署中的挑战,确保应用稳定运行
本文深入探讨了在Docker容器内进行应用调试与故障排除的方法与技巧,包括使用日志、进入容器检查、利用监控工具及检查配置等,旨在帮助用户有效应对应用部署中的挑战,确保应用稳定运行。
359 5
|
运维 监控 Cloud Native
从故障演练到运维工具产品力评测的探索 | 龙蜥技术
随着AI和云原生技术的发展,业界运维工具百花齐放,该如何让优秀的工具脱颖而出?
|
11月前
|
传感器 监控 安全
|
存储 监控 Linux
监控Linux服务器
详细介绍了如何监控Linux服务器,包括监控CPU、内存、磁盘存储和带宽的使用情况,以及使用各种系统监控工具如vmstat、iostat、sar、top和dstat来分析系统性能,并推荐了一些开源监控系统。
235 0
监控Linux服务器
|
移动开发 API 数据处理
构建高效安卓应用:探究Android 12中的新特性与性能优化策略
【4月更文挑战第23天】 随着移动设备的普及,用户对应用程序的性能和效率要求越来越高。安卓系统作为市场占有率最高的移动操作系统之一,其版本更新带来了众多性能提升和新特性。本文将深入探讨Android 12版本中引入的关键性能优化技术,并分析这些技术如何帮助开发者构建更加高效的安卓应用。我们将从最新的运行时权限、后台任务优化、以及电池使用效率等方面入手,提供具体的实践建议,旨在帮助开发者更好地利用这些新工具,以提升应用的响应速度、降低能耗,并最终提高用户的满意度。