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

news2024/9/23 7:26:47

前言

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

  • 悲观锁认为这个数据肯定会被其他线程给修改了,那我就给它上锁,只能自己访问,要等我访问完,其他人才能访问,我上锁、解锁都得花费我时间。

  • 乐观锁认为这个数据不会被修改,我就直接访问,当我发现数据真的修改了,那我也“礼貌的”让自己访问失败。

悲观锁和乐观锁其实本质都是一种思想,在 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/69081.html

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

相关文章

FinClip11月产品更新:FIDE 插件开发功能优化;开发者文档英文版上线

不知不觉 22 年进入尾声&#xff0c;通过一年的不断打磨&#xff0c;FinClip 也在不断成长&#xff0c;现在&#xff0c;让我们看看过去的 11 月&#xff0c;FinClip 又有了哪些新的变化。 产品方面的相关动向&#x1f447;&#x1f447;&#x1f447; FIDE 插件开发功能优化…

【LeetCode每日一题:1775. 通过最少操作次数使数组的和相等~~~贪心+思维题】

题目描述 给你两个长度可能不等的整数数组 nums1 和 nums2 。两个数组中的所有值都在 1 到 6 之间&#xff08;包含 1 和 6&#xff09;。 每次操作中&#xff0c;你可以选择 任意 数组中的任意一个整数&#xff0c;将它变成 1 到 6 之间 任意 的值&#xff08;包含 1 和 6&a…

另一种在ARM/x86架构处理器上部署WebDAV服务器的方法

引言 最近搞了个矿渣&#xff0c;处理器是国产的RK3328&#xff0c;四核A53架构&#xff0c;64位的&#xff0c;性能太好了&#xff0c;装了个OpenWRT&#xff0c;想用来当nas用&#xff0c;但是我发现&#xff0c;竟然没有合适的文件服务器&#xff0c;局域网内用SMB确实可以…

Java并发编程—CompletableFuture的常用方法(建议收藏)

在过去的一段时间里&#xff0c;博主一直在给大家分享多线程并发编程里面的关键CompletableFfuture类的各种技术点&#xff0c;并发编程作为java开发里面关键点之一&#xff0c;也是大家向上提升重要的一点&#xff1b;对于CompletableFuture的学习一定要学到位&#xff0c;前面…

盘点 JDK 中基于 CAS 实现的原子类

前言 JDK 中提供了一系列的基于 CAS 实现的原子类&#xff0c;CAS 的全称是Compare-And-Swap&#xff0c;底层是lock cmpxchg指令&#xff0c;可以在单核和多核 CPU 下都能够保证比较交换的原子性。所以说&#xff0c;这些原子类都是线程安全的&#xff0c;而且是无锁并发&…

第4季5:图像sensor的驱动源码的编译

以下内容源于朱有鹏嵌入式课程的学习与整理&#xff0c;如有侵权请告知删除。 本文演示如何修改sensor的驱动源码&#xff0c;修改之后如何编译与运行。 一、sensor的注册接口分析 这部分内容见第4季4&#xff1a;图像sensor的驱动源码解析。 二、黑电平 关于黑电平的概念&a…

[附源码]计算机毕业设计抗疫医疗用品销售平台Springboot程序

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…

【电巢】电源管理芯片:国产化替代厂家竞逐千亿黄金赛道

前言 整个2022年三季度&#xff0c;全国新能源电动车的起火已高达600多起&#xff0c;同比上升了30%多&#xff0c;如果具体到每天来看&#xff0c;平均每天都有超过7起新能源电动车火灾发生。 7月22日&#xff0c;台湾省专业赛车手林某颖驾驶着一辆白色特斯拉Model X&#xff…

es与mysql之间的数据同步

常见的数据同步方案有三种&#xff1a; 同步调用 异步通知 监听binlog 方案一&#xff1a;同步调用&#xff08;基本不会用&#xff0c;问题太多&#xff09; 基本步骤如下&#xff1a; hotel-demo对外提供接口&#xff0c;用来修改elasticsearch中的数据 酒店管理服务在完成…

活动预告 | DataFunSummit 2022-MLOps 及 AI 工程化落地论坛

11月19日 13&#xff1a;30-17:30&#xff0c;OpenMLDB 项目发起人、第四范式技术副总裁 郑曌作为出品人发起 DataFunSummit 2022 MLOps 及 AI 工程化落地论坛。OpenMLDB PMC、第四范式资深系统架构科师 卢冕&#xff0c;将在论坛中为大家带来议题为《开源机器学习数据库 OpenM…

吃透阿里P8推荐424页Java服务端研发知识图谱后,直接入职蚂蚁P6

前言 蓦然回首自己做开发已经十年了&#xff0c;这十年中我获得了很多&#xff0c;技术能力、培训、出国、大公司的经历&#xff0c;还有很多很好的朋友。但再仔细一想&#xff0c;这十年中我至少浪费了五年时间&#xff0c;这五年可以足够让自己成长为一个优秀的程序员&#…

看场景、重实操,实时数仓不是“纸上谈兵”

本文转载自阿里云Hologres产品负责人合一在ITPUB的访谈&#xff0c;谈谈他眼中的实时数仓&#xff0c; 原文链接&#xff1a; https://mp.weixin.qq.com/s/RZMWf9r4fKV9mNoGGUtaVw 这两年&#xff0c;企业IT领域掀起实时数仓热潮。然而&#xff0c;只要稍做梳理就会发现&#…

【笔试强训】Day 6

&#x1f308;欢迎来到笔试强训专栏 (꒪ꇴ꒪(꒪ꇴ꒪ )&#x1f423;,我是Scort目前状态&#xff1a;大三非科班啃C中&#x1f30d;博客主页&#xff1a;张小姐的猫~江湖背景快上车&#x1f698;&#xff0c;握好方向盘跟我有一起打天下嘞&#xff01;送给自己的一句鸡汤&#x…

Redis - Redis为什么这么快?

1. Redis为什么这么快&#xff1f; 数据结构简单&#xff0c;对数据操作也简单&#xff0c;Redis 中的数据结构是专门进行设计的&#xff1b;基于内存实现&#xff0c;读写速度快&#xff1b;Redis 是单线程的&#xff0c;避免了不必要的上下文切换和竞争条件&#xff0c;也不…

卖座网站影院界面:优化长列表的滑动流畅度方案

关键点1&#xff1a;优化长列表的滑动流畅度 问题&#xff1a;在长列表中&#xff0c;向下滑动时&#xff0c;为了提高用户的使用感受&#xff0c;这个滑动一般都会做的很流畅。但是在网速不快的情况下&#xff0c;它自然会卡顿&#xff0c;所以为了优化网速慢带来的卡顿现象&…

好用的国产远程控制软件,我只推荐这款!

近年来&#xff0c;越来越多的人需要远程办公&#xff0c;远程为用户提供服务或支持等&#xff0c;导致人们对远程控制软件的需求不断增加。 但现在市面上远程控制软件参差不齐&#xff0c;有的远程控制软件功能不齐全&#xff0c;有的操作步骤过于繁琐&#xff0c;有的使用起…

vue 组件封装——可自由拖拽移动的盒子

最终效果 完整代码 关键性要点&#xff0c;详见注释 组件封装 superBox.vue <template><divref"box_Ref"class"box"mousedown"moveStart"mousemove"moving"mouseup"moveEnd":style"{width: (nodeInfo.width…

js模块化

第1章:模块化入门 1.1.理解什么是模块 将一个复杂的程序依据一定的规则拆分成单个文件&#xff0c;并最终组合在一起这些拆分的文件就是模块&#xff0c;模块内部数据是私有的&#xff0c;只是向外部暴露一些方法与外部其它模块通信 1.2.为什么要模块化? 降低复杂度&#…

2023最新SSM计算机毕业设计选题大全(附源码+LW)之java网络游戏虚拟交易平台8rfnp

计算机毕业设计的编程真的不会, 应该怎么办啊, 平时学了3年都没学懂&#xff0c;然而还有一个月就要答辩了&#xff0c;一点东西都没做出来&#xff0c;不知道该怎么办了&#xff1f;好迷茫&#xff0c;如果毕不到业怎么办, 有没有快速的学习方法 毕设毕设&#xff0c;最终的是…

【软件测试】10年测试老鸟,告诉你测试内功修炼之道......

目录&#xff1a;导读前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结&#xff08;尾部小惊喜&#xff09;前言 一名测试人员&#…