乐观锁思想在JAVA中的实现——CAS

news2024/11/17 9:38:39

前言

生活中我们看待一个事物总有不同的态度,比如半瓶水,悲观的人会觉得只有半瓶水了,而乐观的人则会认为还有半瓶水呢。很多技术思想往往源于生活,因此在多个线程并发访问数据的时候,有了悲观锁和乐观锁。

  • 悲观锁认为这个数据肯定会被其他线程给修改了,那我就给它上锁,只能自己访问,要等我访问完,其他人才能访问,我上锁、解锁都得花费我时间。
  • 乐观锁认为这个数据不会被修改,我就直接访问,当我发现数据真的修改了,那我也“礼貌的”让自己访问失败。

悲观锁和乐观锁其实本质都是一种思想,在JAVA中对于悲观锁的实现大家可能都很了解,可以通过synchronizedReentrantLock加锁实现,本文不展开讲解了。那么乐观锁在JAVA中是如何实现的呢?底层的实现机制又是什么呢?

问题引入

我们用一个账户取钱的例子来说明乐观锁和悲观锁的问题。

public class AccountUnsafe {
     // 余额
     private Integer balance;
    
     public AccountUnsafe(Integer balance) {
     	this.balance = balance;
     }
    
    @Override
     public Integer getBalance() {
     	return balance;
     }
    
     @Override
     public void withdraw(Integer amount) {
     	balance -= amount;
     }
}
复制代码
  • 账户类,withdraw()方法是取钱方法。
public static void main(String[] args) {
        // 账户10000元
        AccountUnsafe account = new AccountUnsafe(10000);
        List<Thread> ts = new ArrayList<>();
        long start = System.nanoTime();
        // 1000个线程,每次取10元
        for (int i = 0; i < 1000; i++) {
            ts.add(new Thread(() -> {
                account.withdraw(10);
            }));
        }
        ts.forEach(Thread::start);
        ts.forEach(t -> {
            try {
                t.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        long end = System.nanoTime();
        // 打印账户余额和花费时间
        log.info("账户余额:{}, 花费时间: {}", account.getBalance(), (end-start)/1000_000 + " ms");
    }
复制代码
  • 账户默认有10000元,1000个线程取钱,每次取10元,最后账户应该还有多少钱呢?

运行结果:

  • 运行结果显示余额还有150元,显然出现并发问题

原因分析:

原因也很简单,取钱方法withdraw()的操作balance -= amount;看着就一行代码,实际上会生成多条指令,如下图所示:

多个线程运行的时候会进行线程切换,导致这个操作不是原子性,所以不是线程安全的。

悲观锁解决

最简单的方法,我想大家都能想到吧,给withdraw()方法加锁,保证同一时刻只有一个线程能够执行这个方法,保证了原子性。

  • 通过synchronized关键字加锁。

运行结果:

  • 运行结果正常,但是花费时间稍微多了一点

乐观锁解决

关键来了,如果用乐观锁的思想在JAVA中该如何实现呢?

大致思路就是我默认不加任何锁,我先把余额减掉10元,最后更新余额的时候,发现余额和我一开始不一样了,我就丢弃当前更新操作,重新读取余额的值,直到更新成功。

找啊找,最终发现JDK中的Unsafe方法提供了这样的方法compareAndSwapInt

  • 先获取老的余额oldBalance,计算出新的余额newBalance
  • 调用 unsafe.compareAndSwapInt()方法,如果内存中余额属性的偏移量BALANCE_OFFSET对应的值等于老的余额,说明的确没有被其他线程访问修改过,我就大胆的更新为newBalance,退出方法
  • 否则的话,我就要进入下一次循环,重新获取余额计算。

那么是如何获取unsafe呢?

  • 静态方法中通过反射的方法获取,因为Unsafe类太底层了,它一般不建议程序员直接使用。

这个Unsafe类的名称并不是说线程不安全的意思,只是这个类太底层了,不要乱用,对程序员来说不大安全。

最后别忘了余额balance要加volatile修饰。

  • 主要为了保证可见性,让线程能够获取到其他线程修改的结果。

运行结果:

  • 余额也为0,正常,而且运行速度稍微快了一丢丢

完成代码:

@Slf4j(topic = "a.AccountCAS")
public class AccountCAS {
    // 余额
    private volatile int balance;
    // Unsafe对象
    static final Unsafe unsafe;
    // balance 字段的偏移量
    static final long BALANCE_OFFSET;
    static {
        try {
            Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
            theUnsafe.setAccessible(true);
            unsafe = (Unsafe) theUnsafe.get(null);
            // balance 属性在 AccountCAS 对象中的偏移量,用于 Unsafe 直接访问该属性
            BALANCE_OFFSET = unsafe.objectFieldOffset(AccountCAS.class.getDeclaredField("balance"));
        } catch (NoSuchFieldException | IllegalAccessException e) {
            throw new Error(e);
        }
    }

    public AccountCAS(Integer balance) {
        this.balance = balance;
    }

    public int getBalance() {
        return balance;
    }

    public void withdraw(Integer amount) {
        // 自旋
        while (true) {
            // 获取老的余额
            int oldBalance = balance;
            // 获取新的余额
            int newBalance = oldBalance - amount;
            // 更新余额,BALANCE_OFFSET表示balance属性的偏移量, 返回true表示更新成功, false更新失败,继续更新
            if(unsafe.compareAndSwapInt(this, BALANCE_OFFSET, oldBalance, newBalance)) {
                return;
            }
        }
    }

    public static void main(String[] args) {
        // 账户10000元
        AccountCAS account = new AccountCAS(10000);
        List<Thread> ts = new ArrayList<>();
        long start = System.nanoTime();
        // 1000个线程,每次取10元
        for (int i = 0; i < 1000; i++) {
            ts.add(new Thread(() -> {
                account.withdraw(10);
            }));
        }
        ts.forEach(Thread::start);
        ts.forEach(t -> {
            try {
                t.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        long end = System.nanoTime();
        // 打印账户余额和花费时间
        log.info("账户余额:{}, 花费时间: {}", account.getBalance(), (end-start)/1000_000 + " ms");
    }
}
复制代码

乐观锁改进

好麻烦呀,我们自己调用原生的UnSafe类实现乐观锁,有什么更好的方式吗?

当然有,其实JDK给我们封装了很多基于UnSafe乐观锁实现的原子类,比如AtomicIntegerAtomicReference等等。我们用AtomicInteger改写下上面的实现。

  • 使用JDK中的原子类AtomicInteger作为余额的类型
  • 取钱逻辑直接调用addAndGet方法

运行结果:

原理:

查看源码最终也是调用的Unsafe方法。

CAS机制

前面的一个取钱的例子,大家是不是对乐观锁的思想以及在JAVA中的实现更深入的认识。

在JAVA中对这种实现起了一个名字,叫做CAS, 全称Compare And Swap,是不是很形象,先比较,然后再替换。

那CAS的本质是什么?

CAS先比较然后再替换,感觉是有2步,比较和替换,不像是原子性操作,如果不是原子性操作问题就可大了。实际上,CAS本质对应的是一条指令,是原子操作

CAS 的底层是 lock cmpxchg 指令(X86 架构),在单核 CPU 和多核 CPU 下都能够保证【比较-交换】的原子性。

强调一点,CAS 必须借助 volatile 才能读取到共享变量的最新值来实现【比较并交换】的效果,因为volatile会保证变量的可见性。

总结

结合 CAS 和 volatile 可以实现无锁并发,适用于线程数少、多核 CPU 的场景或者读多写少的场景。

  • CAS 是基于乐观锁的思想:最乐观的估计,不怕别的线程来修改共享变量,就算改了也没关系,我吃亏点再重试呗。
  • synchronized 是基于悲观锁的思想:最悲观的估计,得防着其它线程来修改共享变量,我上了锁你们都别想改,我改完了解开锁,你们才有机会。
  • CAS 体现的是无锁并发、无阻塞并发,请仔细体会这两句话的意思
    • 因为没有使用 synchronized,所以线程不会陷入阻塞,这是效率提升的因素之一
    • 但如果竞争激烈,可以想到重试必然频繁发生,反而效率会受影响

如果本文对你有帮助的话,请留下一个赞吧

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

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

相关文章

React+ts学习文档

1.项目中遇到的困难解决以及方案 1.顶部的查询按钮 点击查询如果在该组件去进行axios请求&#xff0c;这样再该组件下获得返回的列表还需要传回父组件&#xff0c;父组件再把列表发给下面的table组件&#xff0c;不太方便。 解决方案&#xff1a;将用户的选择项传给顶级组件i…

Kubernetes v1.25 搭建一个单节点集群用于Debug K8S源码

参考说明 参考自&#xff1a;v1.25.0-CentOS-binary-install-IPv6-IPv4-Three-Masters-Two-Slaves.md&#xff0c;按照自己的理解修改了下。 搭建好的单节点v1.25.4版本集群 1. 集群环境准备 1.1. 主机规划 IP主机名主机角色操作系统安装组件192.168.11.71k8s-master1maste…

在Python中使用LSTM和PyTorch进行时间序列预测

顾名思义&#xff0c;时间序列数据是一种随时间变化的数据类型。例如&#xff0c;24小时内的温度&#xff0c;一个月内各种产品的价格&#xff0c;一年中特定公司的股票价格。 去年&#xff0c;我们为一位客户进行了短暂的咨询工作&#xff0c;他正在构建一个主要基于时间序列…

【✨十五天搞定电工基础】正弦交流电路的分析

本章要求 1. 理解正弦量的特征及其各种表示方法&#xff1b; 2. 理解电路基本定律的相量形式及阻抗&#xff1b;熟练掌握计算正弦交流电路的相量分析法&#xff0c; 会画相量图&#xff1b; 3. 掌握有功功率和功率因数的计算,了解瞬时功率、无功功率和视在功率的概…

项目管理(如何进行团队管理)

团队构建与管理6步走 各项工作概述: 获取团队成员需要考虑的因素: 可用性。确认资源能否在项目所需时段内为项目所用。 成本。确认增加资源的成本是否在规定的预算内。 能力。确认团队成员是否提供了项目所需的能力。 有些选择标准对团队资源来说是独特的,包括: 经验。…

HTML人物介绍、个人设计web前端大作业、贝聿铭人物介绍(带报告3000字)

&#x1f389;精彩专栏推荐&#x1f447;&#x1f3fb;&#x1f447;&#x1f3fb;&#x1f447;&#x1f3fb; ✍️ 作者简介: 一个热爱把逻辑思维转变为代码的技术博主 &#x1f482; 作者主页: 【主页——&#x1f680;获取更多优质源码】 &#x1f393; web前端期末大作业…

Allegro如何使用快捷键快速切换走线线宽操作指导

Allegro如何使用快捷键快速切换走线线宽操作指导 Allegro可以用快捷键快速切换走线线宽,比如在command下方输入数字5,可以切换到5mil的线宽 具体操作如下 打开系统属性,选择环境变量 找到home的环境变量的路径是哪里 找到路径下的pcbenv文件夹 找到env文件 用记事本打…

Android NDK初识

Android NDK是Android软件开发包(SDK)的相关工具集&#xff0c;用来扩展Android SDK的功能&#xff0c;从而使开发人员能够使用机器代码生成的编程语言(如C、C和汇编语言)实现一些对代码性能要求较高的模块&#xff0c;并将这些模块嵌入到Android应用程序中使用。 什么是Andro…

工业物联网案例:智能工厂设备无人值守系统方案

智能工厂又称“黑灯工厂”&#xff0c;具备智能化、自动化、无人化等特点&#xff0c;可以实现全天候高效的生产工作&#xff0c;是制造业的先进典范。通过工厂设备无人值守系统&#xff0c;可以集中采集、传输、处理、显示各类工业设备数据&#xff0c;可以对空压机、变电站、…

Gitee配置静态页面

辛辛苦苦写好了一个静态网页&#xff0c;想让大家都可以通过网络访问看到这个网页。比如&#xff0c;个人简历&#xff0c;个人静态页面作品展示等。 但是不想买服务器&#xff0c;配置域名&#xff0c;备案&#xff0c;什么的。 可以使用 Gitee Pages 服务&#xff0c;将静态…

Qt-FFmpeg开发-打开本地摄像头录制视频(7)

Qt-FFmpeg开发-打开本地摄像头录制视频【软解码 OpenGL显示YUV】 文章目录Qt-FFmpeg开发-打开本地摄像头录制视频【软解码 OpenGL显示YUV】1、概述2、实现效果3、FFmpeg录制视频编码流程4、主要代码5、完整源代码更多精彩内容&#x1f449;个人内容分类汇总 &#x1f448;&…

【车辆动力】基于matlab模拟停车动力学【含Matlab源码 2258期】

⛄一、获取代码方式 获取代码方式1: 完整代码已上传我的资源:【车辆动力】基于matlab模拟停车动力学【含Matlab源码 2258期】 点击上面蓝色字体,直接付费下载,即可。 获取代码方式2: 付费专栏物理应用(Matlab) 备注: 点击上面蓝色字体付费专栏物理应用(Matlab),扫…

【教学类-20-02】20221203《世界杯16强国旗-定量版》(大班)

展示效果&#xff1a; &#xff08;1个国家2张&#xff0c;16国旗&#xff0c;共32张&#xff09; 打印效果&#xff1a; 背景需求&#xff1a; 上一份代码打印后发现&#xff1a; 1.打印时发现随机抽取的图案不稳定&#xff0c;30张”澳大利亚”“波兰”的图片特别多。因为…

STM32定时器笔记

学习江科大自化协的stm32教程记录的笔记 一、TIM定时器 定时器可以对输入的时钟进行计数&#xff0c;并在计数值达到设定值时触发中断 例&#xff1a;stm32中定时器的基准时钟一般是72MHZ&#xff0c;【周期是频率的倒数1T 1/72us】&#xff0c;如果计数72个&#xff0c;就是…

[附源码]Python计算机毕业设计Django松林小区疫情防控信息管理系统

项目运行 环境配置&#xff1a; Pychram社区版 python3.7.7 Mysql5.7 HBuilderXlist pipNavicat11Djangonodejs。 项目技术&#xff1a; django python Vue 等等组成&#xff0c;B/S模式 pychram管理等等。 环境需要 1.运行环境&#xff1a;最好是python3.7.7&#xff0c;…

Netty(三)- NIO三大组件之Channel

文章目录一、Channel 基本介绍二、FileChannel 类三、Channel 应用案例1. 应用实例 1 - 本地文件写数据2. 应用实例 2 - 本地文件读数据3. 应用实例 3 - 使用一个Buffer 完成文件读取、写入4. 应用实例 4 - 拷贝文件transferFrom方法一、Channel 基本介绍 NIO的通道类似于流&am…

Mybatis:自定义映射resultMap(7)

Mybaits笔记框架&#xff1a;https://blog.csdn.net/qq_43751200/article/details/128154837 自定义映射resultMap1. Mybatis环境搭建2. 问题引入3. 解决表中的字段名和对应类的属性名不一致问题方式一&#xff1a; 为字段起别名&#xff0c;保持和属性名的一致方式二&#xff…

html静态网站基于HTML+CSS+JavaScript上海美食介绍网站网页设计与实现共计5个页面

&#x1f380; 精彩专栏推荐&#x1f447;&#x1f3fb;&#x1f447;&#x1f3fb;&#x1f447;&#x1f3fb; ✍️ 作者简介: 一个热爱把逻辑思维转变为代码的技术博主 &#x1f482; 作者主页: 【主页——&#x1f680;获取更多优质源码】 &#x1f393; web前端期末大作业…

K邻近算法k值选取以及kd树概念、原理、构建方法、最近邻域搜索和案例分析

一、k值选择 K值过小&#xff1a;容易受到异常点的影响k值过大&#xff1a;受到样本均衡的问题 近似误差&#xff1a;对现有训练集的训练误差&#xff0c;关注训练集&#xff0c;如果近似误差过小可能会出现过拟合的现象&#xff0c;对现有的训练集能有很好的预测&#xff0c;…

基于RockyLinux8.7一键安装OpenStack Yoga版本

硬件环境 虚拟软件&#xff1a;vmware workstation16 操作系统&#xff1a;RockyLinux8 虚拟机硬件配置&#xff1a; CPU&#xff1a;2 memory&#xff1a;8G disk&#xff1a;80G net card&#xff1a;1个—VMnet8 ip/netmask&#xff1a;192.168.9.160/24 下载并安装RockyL…