Java多线程-StampedLock(原子读写锁)

news2025/7/9 19:04:55

StampedLock 是读写锁的实现,对比 ReentrantReadWriteLock 主要不同是该锁不允许重入,多了乐观读的功能,使用上会更加复杂一些,但是具有更好的性能表现。StampedLock 的状态由版本和读写锁持有计数组成。 获取锁方法返回一个邮戳,表示和控制与锁状态相关的访问; 这些方法的“尝试”版本可能会返回特殊值 0 来表示获取锁失败。 锁释放和转换方法需要邮戳作为参数,如果它们与锁的状态不匹配则失败。

但是也是由于 StampedLock 大量使用自旋的原因(ReentrantReadWriteLock 也使用了自旋,但是没有 StampedLock 频繁),CPU 的消耗理论上也比 ReentrantReadWriteLock 高。

StampedLock 非常适合写锁中的操作非常快的业务场景。因为读锁如果因为写锁而获取锁失败,读锁会做重试获取和有限次的自旋的方式,比较晚进入到等待队列中。如果在自旋过程中,写锁能释放,那么获取读锁的线程就能避免被操作系统阻塞和唤醒等耗资源操作,增加读锁的响应效率。

三种模式

悲观读锁

与 ReentrantReadWriteLock 的读锁类似,多个线程可以同时获取悲观读锁。这是一个共享锁,允许多个线程同时读取共享资源。

乐观读锁

相当于直接操作数据,不加任何锁。在操作数据前并没有通过 CAS 设置锁的状态,仅仅通过位运算测试。如果当前没有线程持有写锁,则简单地返回一个非 0 的 stamp 版本信息。返回 0 则说明有线程持有写锁。获取该 stamp 后在具体操作数据前还需要调用 validate 方法验证该 stamp 是否己经不可用。

写锁

与 ReentrantReadWriteLock的写锁类似,写锁和悲观读锁是互斥的。虽然写锁与乐观读锁不会互斥,但是在数据被更新之后,之前通过乐观读锁获得的数据已经变成了脏数据,需要自己处理这个。

StampedLock 的读写锁都是不可重入锁,所以在获取锁后释放锁前不应该再调用会获取锁的操作,以避免造成调用线程被阻塞。

在实际应用中,StampedLock 可以用于那些读操作远多于写操作的场景,例如缓存系统、数据报表生成等。在这些场景中,StampedLock 可以显著提高并发性能,同时保证数据的一致性和安全性。

最重要的一点: 在使用时需要特别注意:如果某个线程阻塞在StampedLock的readLock()或者writeLock()方法上时,此
时调用阻塞线程的interrupt()方法中断线程,会导致CPU飙升到100%。

所以尽量在写操作是非常快的场景下使用, 这样读的时候乐观锁释放的非常快,几乎达到无锁模式。

所有接口方法

image.png
image.png
image.png

经典案例

import java.util.concurrent.locks.StampedLock;

public class StampedLockExample {
    private int inventory = 100; // 初始库存为100
    private final StampedLock lock = new StampedLock();

    // 扣减库存操作
    public void decreaseInventory(int quantity) {
        long stamp = lock.writeLock(); // 获取写锁
        try {
            if (inventory >= quantity) {
                inventory -= quantity; // 扣减库存
                System.out.println("成功减少库存 " + quantity + ", 当前的库存量: " + inventory);
            } else {
                System.out.println("未能减少库存,库存不足");
            }
        } finally {
            lock.unlockWrite(stamp); // 释放写锁
        }
    }

    // 获取当前库存
    public int getInventory() {
        long stamp = lock.tryOptimisticRead(); // 乐观读锁
        int currentInventory = inventory;
        if (!lock.validate(stamp)) { // 检查乐观读锁是否有效
            stamp = lock.readLock(); // 乐观读锁无效,转为悲观读锁
            try {
                currentInventory = inventory; // 获取当前库存
            } finally {
                lock.unlockRead(stamp); // 释放读锁
            }
        }
        return currentInventory; // 返回当前库存
    }

    public static void main(String[] args) {
        StampedLockExample manager = new StampedLockExample();
        // 多个线程同时扣减库存
        Thread t1 = new Thread(() -> {
            manager.decreaseInventory(20); // 线程1扣减库存
            System.out.println(manager.getInventory());
        });
        Thread t2 = new Thread(() -> {
            manager.decreaseInventory(50); // 线程2扣减库存
            System.out.println(manager.getInventory());
        });
        t1.start();
        t2.start();
    }
}

官网案例

 
public class Point {
    private double x, y;
    private final StampedLock sl = new StampedLock();
 
    public void move(double deltaX, double deltaY) {
        使用写锁-独占操作,并返回一个邮票
        long stamp = sl.writeLock();
        try {
            x += deltaX;
            y += deltaY;
        } finally {
            使用邮票来释放写锁
            sl.unlockWrite(stamp);      
        }
    }
 
    
    // 使用乐观读锁访问共享资源
    // 注意:乐观读锁在保证数据一致性上需要拷贝一份要操作的变量到方法栈,并且在操作数据时候可能其 
    // 他写线程已经修改了数据,而我们操作的是方法栈里面的数据,也就是一个快照,所以最多返回的不是 
    // 最新的数据,但是一致性还是得到保障的。
    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);
    }
 
 
    // 锁升级:读锁--> 写锁
    public void moveIfAtOrigin(double newX, double newY) { // 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);
        }
    }
 
 
}

StampedLock和ReentrantReadWriteLock之间的区别

  1. 锁的类型与特性
    • StampedLock:提供了乐观读、悲观读和写锁三种模式。乐观读模式允许在写锁未被持有时进行无锁读取,通过验证戳记(stamp)来确保数据的一致性。这种模式减少了锁的竞争,提高了吞吐量。
    • ReentrantReadWriteLock:允许多个读线程同时访问,但写线程在访问时必须独占。它支持锁的重入,即同一线程可以多次获取同一把锁。
  2. 性能
    • StampedLock:通常比ReentrantReadWriteLock具有更高的性能,特别是在读多写少的场景下。由于乐观读的存在,它能够在无竞争的情况下避免不必要的锁开销。
    • ReentrantReadWriteLock:在读操作远多于写操作的场景中表现良好,但写锁的饥饿问题和锁降级操作可能影响其性能。
  3. 实现机制
    • StampedLock:并非基于AQS(AbstractQueuedSynchronizer)实现,而是使用了自己的同步等待队列和状态设计。其状态为一个long型变量,与ReentrantReadWriteLock的设计不同。
    • ReentrantReadWriteLock:基于AQS实现,通过内部维护的读写锁来实现多线程间的同步。
  4. 使用场景
    • StampedLock:更适合于读多写少且对性能要求较高的场景,尤其是当数据争用不严重时。它能够有效减少锁的竞争,提高系统的吞吐量。
    • ReentrantReadWriteLock:适用于需要重入锁或需要在写操作后降级为读锁的场景。它提供了更严格的访问控制,但可能在某些情况下牺牲了一定的性能。
  5. 锁的获取与释放
    • StampedLock:在获取锁时会返回一个戳记(stamp),用于后续的锁释放或转换。这个戳记代表了锁的状态,有助于在释放锁时验证数据的一致性。
    • ReentrantReadWriteLock:没有戳记的概念,锁的获取和释放相对简单直接。

综上所述,StampedLock和ReentrantReadWriteLock各有其特点和适用场景。在选择使用哪种锁时,应根据具体的应用需求和性能要求来做出决策。

点赞 -收藏 -关注
有问题在评论区或者私信我-收到会在第一时间回复

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1822225.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

源代码防泄密经验分享之安全上网篇

场景描述: 随着信息技术的发展,越来越多的新技术产品进入到政府、军事、科研等涉密单位。这些新技术产品在给工作人员带来便利的同时,也给信息安全保密工作带来了许多新的不容忽视的安全隐患,应引起高度重视。常规的内外网隔离手…

VSCode插件开发之初始化项目

VS code常见组件 在VS Code插件开发中,常用的组件有很多,这些组件可以帮助你实现各种功能和交互。以下是一些常见的组件: Extension API模块: 提供了许多类和方法,用于与VS Code编辑器进行交互,例如vscode.workspace用…

抽象语法树AST(Abstract Syntax Tree)

抽象语法树(Abstract Syntax Tree) 抽象语法树(Abstract Syntax Tree,AST)是源代码语法结构的一种抽象表示它以树状的形式表现编程语言的语法结构,树上的每个节点都表示源代码中的一种结构 抽象语法树用途 代码语法的检查、代码…

辣椒属2个T2T基因组-文献精读23

Two telomere-to-telomere gapless genomes reveal insights into Capsicum evolution and capsaicinoid biosynthesis 两个端粒到端粒无缝基因组揭示了辣椒进化和辣椒素生物合成的相关见解 摘要 辣椒(Capsicum)因其果实中含有辣椒素而闻名&#xff0c…

【SQL每日一练】获取北纬度(LAT_N)的中位数

文章目录 前言一、题析二、题解1.mysql2.sqlserver 前言 从 STATION 查询北纬度 (LAT_N) 的中位数,并将您的答案四舍五入到小数点后4位. 中位数的定义是:如果数据量是奇数,则中位数是排序后位于中间的数;如…

拥抱数字世界|AI在娱乐行业的应用,娱乐新纪元已到来

在蓬勃发展的全球化趋势下,越来越多的厂商正在批量涌入娱乐赛道,期待能创造新的增长奇迹。随着科技的不断发展,人工智能技术正日益深入各行各业,其中媒体和娱乐行业更是迎来了一场革命性的变革。在媒体和娱乐领域展现出了巨大的潜…

海康威视-NVR使用及ISAPI协议透传接入

目录 1、初始化配置 1.1、设置通道默认密码 1.2、添加摄像头 1.3、设置不采集时间段 1.4、抓拍延迟设置 1.5、录像保存时长设置 1.6、人脸库维护 1.7、导入照片 1.8、设置事件 1.8.1、引擎配置 1.8.2、事件设置 1.8.2.1、目标比对 1.8.2.2、设置屏蔽区 1.8.2.3、…

每日一练:攻防世界:北京地铁

首先是找图片隐写 在这里可以看到一串类似base64格式的字符串 再结合题目,这应该就是明文了,要AES解密,还需要密钥,提示要看图片本身,那密钥可能藏在里面,找了半天没找到,参考师傅的wp&#x…

Docker:利用Docker搭建一个nginx服务

文章目录 搭建一个nginx服务认识nginx服务Web服务器反向代理服务器高性能特点 安装nginx启动nginx停止nginx查找nginx镜像拉取nginx镜像,启动nginx站点其他方式拉取nginx镜像信息通过 DIGEST 拉取镜像 搭建一个nginx服务 首先先认识一下nginx服务: NGI…

FreeRTOS 简单内核实现1 前言

文章目录 0、写在前面1、参考资料2、准备工作2.1、STM32 空工程2.2、创建 RTOS 文件目录 3、约定4、专栏目录5、项目仓库 0、写在前面 为深入理解 RTOS 内核工作机制,笔者制作了名为 “FreeRTOS 内核简单实现” 的项目专栏 ,目标为自己动手从 0 到 1 编…

第二证券股市资讯:苹果,重回第一!

苹果以弱小的优势,从头夺回市值榜首宝座。 当地时间6月13日周四,美股三大股指涨跌纷歧,纳指与标普500指数均录得接连第四日上涨,而且再创前史新高。 周四,美国5月份生产者价格指数(PPI)意外下…

Apache Doris单机快速安装(已踩坑)

官方文档:https://doris.incubator.apache.org/zh-CN/docs/get-starting/quick-start/ 环境: 操作系统:CentOS7.6 X86_64 JDK:Oracle jdk1.8.0_351 1.版本下载 从 doris.apache.org 下载相应的 Doris 安装包,并且解压…

碎片化知识如何被系统性地吸收?

一、方法论 碎片化知识指的是通过各种渠道快速获取的零散信息和知识点,这些信息由于其不完整性和孤立性,不易于记忆和应用。为了系统性地吸收碎片化知识,可以采用以下策略: 1. **构建知识框架**: - 在开始吸收之前&am…

吉时利Keithley2611B单通道SMU数字源表

Keithley吉时利2611B数字源表 2611B、2612B、2614B 系统 Sourcemeter SMU 仪器 2611B、2612B 和 2634B 系统 Sourcemeter SMU 仪器为 30W DC / 200W 脉冲 SMU,支持 10A 脉冲,1.5A 至 100fA 和 200V 至 100nV DC。所有 2600B SMU 均配备吉时利 TSP 脚本…

硬件开发笔记(十八):核心板与底板之间的连接方式介绍说明:板对板连接器

若该文为原创文章,转载请注明原文出处 本文章博客地址:https://hpzwl.blog.csdn.net/article/details/139663096 长沙红胖子Qt(长沙创微智科)博文大全:开发技术集合(包含Qt实用技术、树莓派、三维、OpenCV…

基于SSD的安全帽检测

目录 1. 作者介绍2. SSD算法介绍2.1 SSD算法网络结构2.2 SSD算法训练过程2.3 SSD算法优缺点 3. 基于SSD的安全帽检测实验3.1 VOC 2007安全帽数据集3.2 SSD网络架构3.3 训练和验证所需的2007_train.txt和2007_val.txt文件生成3.4 模型训练3.5 GUI界面3.6 结果展示3.7 文件下载 4…

C#版 iText7——画发票PDF(完整)

显示描述&#xff1a; 1、每页显示必须带有发票头、“销售方和购买方信息” 2、明细填充为&#xff1a;当n≤8 行时&#xff0c;发票总高度140mm&#xff0c;每条发票明细行款高度4.375mm&#xff1b; 当8<n≤12行时&#xff0c;发票高度增加17.5mm&#xff0c;不换页&#…

人工智能内容生成元年-AI绘画原理解析

随着人工智能技术的飞速发展&#xff0c;AI绘画作为其引人注目的应用领域&#xff0c;正在以惊人的速度崭露头角。从最初的生成对抗网络&#xff08;GAN&#xff09;到如今的深度学习&#xff0c;AI绘画技术在艺术创作、设计等领域展现出了无限的可能性。其独特的算法和智能化特…

构建 deno/fresh 的 docker 镜像

众所周知, 最近 docker 镜像的使用又出现了新的困难. 但是不怕, 窝们可以使用曲线救国的方法: 自己制作容器镜像 ! 下面以 deno/fresh 举栗, 部署一个简单的应用. 目录 1 创建 deno/fresh 项目2 构建 docker 镜像3 部署和测试4 总结与展望 1 创建 deno/fresh 项目 执行命令…

情侣飞行棋系统微信小程序+H5+微信公众号+APP 源码

情侣飞行棋系统&#xff1a;浪漫与策略并存的双人游戏 &#x1f3b2; 一、引言&#xff1a;寻找爱情的乐趣 在繁忙的生活中&#xff0c;情侣们总是渴望找到一种既能增进感情又能带来乐趣的活动。而“情侣飞行棋系统”正是这样一个完美的选择。它结合了传统飞行棋的玩法和情侣…