多线程的CAS(Compare and Swap)机制与乐观锁、AtomicInteger等原子包装类的使用

news2024/12/28 18:43:58

一.乐观锁 与 CAS机制

        在java的多线程并发过程中:
​         1.当一个对象在多个内存中都存在副本时,如果一个线程在自己的工作内存修改了共享变量,其它线程也应该能够看到被修改后的值。常常用volatile关键字来保证多线程数据的可见性
​         2.保证一个线程操作的数据返回给主存后,再让第二个线程取数据、操作数据,否则会造成经典的银行存取款余额不统一问题。

​         在之前的“11.线程”文章中提过,我们知道,为了保证线程安全,采用增加synchronized同步锁的方式,使得同步操作具有原子性,实现了线程安全。但这种方法对于锁来说其实是一种悲观锁,具有一定的性能问题:

	synchronized关键字会让没有得到锁资源的线程进入BLOCKED状态,而后在争夺到锁资源后恢复为RUNNABLE状态,这个过程中涉及到操作系统用户模式和内核模式的转换,代价比较高。

​         针对此情况,我们可以使用一系列以Atomic开头的包装类:如AtomicBoolean,AtomicInteger,AtomicLong。它们分别用于Boolean,Integer,Long类型的原子性操作,并且不需要加锁,其底层用到的就是CAS机制,这也被称为乐观锁(其实并不是锁),第四节会介绍这些类。

二.CAS机制的原理、实例解析

        cas,即Compare and Set,顾名思义,比较、再更新

2.1 实现步骤 

        在cas的过程中,分为几个步骤,来实现不需要synchronized也能实现线程安全:

  1. 获取当前主存里的值value,到线程的工作内存中后,称为prev;
  2. 对prev进行操作,操作后得到应该更新回去的值,称为next;
  3. 在将next更新回主存的value之前,去主存里取出当前value值,并比较更新前的这个value与第一次拿到的prev是否相同。
  4. 如果相同,说明在步骤 2.的过程中,没有别的线程更改这个value更新主存将是线程安全的;
    反之不安全,线程将进入重新尝试的过程,被称为自旋

2.2 举例说明

        光说步骤比较抽象,可以从下图的例子来理解:

        假设线程1、2都要来对account进行取款,在乐观锁的情况下,线程1先获取value到自己的工作内存,作为prev=100,然后操作后,next=90,此时在要更新回去之前,线程2咔的一下把account减为90了,那么线程1在此时获取的当前account就是90发现与第一次的prev=100不同,说明此时线程1还不能更新,要再次尝试。
        第二次同理,而第三次,更新之前发现account的当前值与prev相同了,则更新回主存。

 2.3 注意volatile

        注意上文的标红文字,在从工作内存更新回主存之前,要获取最新的当前主存的value,因此这个value必须具有可见性,因此必须用volatile关键字进行修饰。
        也就是说,乐观锁必须搭配volatile来进行使用。

2.4 总结

        CAS的过程要从主存获取2次value:
        第一次是为了拿到数据、进行业务操作
        第二次是为判断更新之前主存中该数据有没有发生更改

三.CAS乐观锁的缺点

3.1 cpu开销大

​        在并发量比较高的情况下,如果许多线程反复尝试更新某一个变量,却又一直更新不成功,循环往复的自旋将会给CPU带来很到的压力。(竞争压力过大,那就不用它

3.2 不能保证代码块的原子性

​ CAS机制所保证的只是一个变量的原子性操作,而不能保证整个代码块的原子性。比如需要保证3个变量共同进行原子性的更新,就不得不使用synchronized了。

3.3 小故事 与 经典的“ABA问题”

        小故事:好比说,小王在出门前看见自己老婆在家,回家后发现老婆也在家,但小王出门的这段时间老婆可能先去了老王家又回来了,可能已经被update了!然而小王却发现不了。
        于是聪明的小王给老婆装了个神奇计数器,老婆每去另外一个地方,计数器都会加1分!小王回家一看,计数器还是不是出门前的数字,就知道老婆有没有出门被别人update过几次了!哈哈哈!

​        当从主存中取得一个value(称为A)到线程的工作内存prev后,直到将next返回给主存的过程中间,若其他线程将主存得A变为B、再变为A,会发生什么?
        显然,在next更新到主存之前,主存的这个value仍然是A,所以根据cas规则会进行更新。
        也就是说,单纯的CAS机制只能判断数据是否更改无法判断数据是否被更改过多少次!

        这将会带来以下等问题:

  1. 数据不一致性:尽管看起来最终结果没有变化,但是如果其他的线程依赖于 X 的值并在此期间进行操作,可能会产生与预期不一致的结果。这意味着某些操作可能依赖于中间状态的 X 值,而非最终结果。

  2. 死循环:在实际应用中,ABA 问题可能导致死循环的发生。例如,一个线程可能会反复检查变量 X 的值,然后判断它是否发生了变化。尽管最终结果没有变化,但由于发生了 ABA 问题,线程可能会错误地认为变量 X 发生了变化,从而导致死循环。

  3. 逻辑错误:ABA 问题可能导致在进行某些操作之前做出错误的判断。如果线程基于错误的假设进行操作,可能会导致逻辑上的错误结果。

        解决方法是给每一次变量编版本号,每修改一次,版本号加1。这样CAS就能根据版本号判断是否数据是否被更改过。

//不考虑版本号
static AtomicReference<String> ref = new AtomicReference<>("A");

//考虑版本号的方法
public class atomic {
    //版本号初始化为0
    static AtomicStampedReference<String> ref = new AtomicStampedReference<>("A", 0);
    public static void main(String[] args) throws InterruptedException {
        log.debug("main start...");
        
        // 获取值 A
        String prev = ref.getReference();
        
        // 获取版本号
        int stamp = ref.getStamp();
        log.debug("版本 {}", stamp);
        // 如果中间有其它线程干扰,发生了 ABA 现象
        other();
        sleep(1);
        // 尝试改为 C,由于Other方法内对A操作了2次,版本号为2 !=0 因此更新失败
        log.debug("change A->C {}", ref.compareAndSet(prev, "C", stamp, stamp + 1));
    }
    private static void other() {
        new Thread(() -> {
            log.debug("change A->B {}", ref.compareAndSet(ref.getReference(), "B",
                    ref.getStamp(), ref.getStamp() + 1));//更新后版本号+1
            log.debug("更新版本为 {}", ref.getStamp());
        }, "t1").start();
        sleep(0.5);
        new Thread(() -> {
            log.debug("change B->A {}", ref.compareAndSet(ref.getReference(), "A",
                    ref.getStamp(), ref.getStamp() + 1));
            log.debug("更新版本为 {}", ref.getStamp());
        }, "t2").start();
    }
}

四.  AtomicBoolean,AtomicInteger,AtomicLong的使用

        对于这些类的对象,有专门的api来进行数据的操作,都能够自动保证原子性与线程安全,本文限于篇幅,只着重介绍常用的AtomicInteger,其他是类似的。
        例如上面第2节的account例子的代码:   

private AtomicInteger balance;
......
while (true) {
     int prev = balance.get();
     int next = prev - amount;
     if (balance.compareAndSet(prev, next)) {
        //compareAndSet即cas函数,如果修改成功会返回true
         break;
     }
 }

         该段代码可以简化为这一步,就可以保证线程安全。

balance.addAndGet(-1 * amount);

以下是常用api,节选自Black-Horse的pdf内容,供参考

AtomicInteger i = new AtomicInteger(0);
// 获取并自增(i = 0, 结果 i = 1, 返回 0),类似于 i++
System.out.println(i.getAndIncrement());
// 自增并获取(i = 1, 结果 i = 2, 返回 2),类似于 ++i
System.out.println(i.incrementAndGet());
// 自减并获取(i = 2, 结果 i = 1, 返回 1),类似于 --i
System.out.println(i.decrementAndGet());
// 获取并自减(i = 1, 结果 i = 0, 返回 1),类似于 i--
System.out.println(i.getAndDecrement());
// 获取并加值(i = 0, 结果 i = 5, 返回 0)
System.out.println(i.getAndAdd(5));
// 加值并获取(i = 5, 结果 i = 0, 返回 0)
System.out.println(i.addAndGet(-5));
// 获取并更新(i = 0, p 为 i 的当前值, 结果 i = -2, 返回 0)
// 其中函数中的操作能保证原子,但函数需要无副作用
System.out.println(i.getAndUpdate(p -> p - 2));
// 更新并获取(i = -2, p 为 i 的当前值, 结果 i = 0, 返回 0)
// 其中函数中的操作能保证原子,但函数需要无副作用
System.out.println(i.updateAndGet(p -> p + 2));
// 获取并计算(i = 0, p 为 i 的当前值, x 为参数1, 结果 i = 10, 返回 0)
// 其中函数中的操作能保证原子,但函数需要无副作用
// getAndUpdate 如果在 lambda 中引用了外部的局部变量,要保证该局部变量是 final 的
// getAndAccumulate 可以通过 参数1 来引用外部的局部变量,但因为其不在 lambda 中因此不必是 final
System.out.println(i.getAndAccumulate(10, (p, x) -> p + x));
// 计算并获取(i = 10, p 为 i 的当前值, x 为参数1, 结果 i = 0, 返回 0)
// 其中函数中的操作能保证原子,但函数需要无副作用
System.out.println(i.accumulateAndGet(-10, (p, x) -> p + x));


        

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

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

相关文章

Nginx Rewrite 重写跳转

文章目录 一.Nginx Rewrite概述1.Rewrite跳转场景2.Rewrite跳转场景3.Rewrite跳转实现4.Rewrite实际场景4.1Nginx跳转需求的实现方式4.2 rewrite放在 server{}&#xff0c;if{}&#xff0c;location{} 段中4.3对域名或参数字符串 5.nginx正则表达式5.1 常用的正则表达式元字符 …

Jmeter(jmeter-plugins插件的安装使用)

目录 一、安装JMter Plugins 二、Custom Thread Groups插件 Stepping Thread Group 元件 Ultimate Thread Group 一、安装JMter Plugins 1、官网下载 JMeter Plugins 的jar包 2. 将下载的jar包复制到 %JMETER_HOME%\lib\ext 目录下 3. 启动 JMeter --> Options -->…

学习系统编程No.26【信号处理实战】

引言&#xff1a; 北京时间&#xff1a;2023/6/26/13:35&#xff0c;昨天12点左右睡觉&#xff0c;本以为能和在学校一样&#xff0c;7点左右起床&#xff0c;设置了7点到8点30时间段内的4个闹钟&#xff0c;可惜没想到啊&#xff0c;没醒&#xff0c;直接睡到了12点&#xff…

rust基本语法

文章目录 变量与可变性变量与常量Shadowing&#xff08;隐藏&#xff09;数据类型标量类型1.整数类型2.浮点类型3.布尔类型4.字符类型 复合类型1.Tuple2.数组 函数if表达式循环1.loop2.while3.for 变量与可变性 声明变量使用let关键字&#xff0c;默认情况下&#xff0c;变量是…

kafka初学入门

kafka概述 消息中间件对比 特性ActiveMQRabbitMQRocketMQKafka开发语言javaerlangjavascala单机吞吐量万级万级10万级100万级时效性msusmsms级以内可用性高&#xff08;主从&#xff09;高&#xff08;主从&#xff09;非常高&#xff08;分布式&#xff09;非常高&#xff0…

大模型显存占用分析

大模型显存占用由以下几部分组成&#xff1a; 1. 模型本身参数&#xff0c;假设是1个单位 2.模型的梯度&#xff0c;同样也是一个单位 3.优化器参数&#xff08;占大头&#xff09;&#xff1a;以Adam参数为例&#xff0c;还需要在显卡中额外存储m和v两个参数&#xff0c;因…

File学习

1.构造方法 1.File(String pathname) 根据路径名创建抽象File对象 //1. 通过路径进行创建 pathname是字符串格式的路径名public File(String pathname) {if (pathname null) {throw new NullPointerException();}// 和系统交互 获取最近的File文件目录文件this.path fs.nor…

10分钟快速入门UI自动化-Puppeteer

目录 先简单介绍一下&#xff1a; 工欲善其事必先利其器&#xff0c;首先把所需要的工具装好 1. 安装node 2. 安装npm &#xff08;node安装时会自动安装npm,如果已安装node&#xff0c;此步请忽略) 3. 安装cnpm (npm下载包失败&#xff0c;选择cnpm安装) 4. 新建一个nod…

【ICer必备 4】IC封装设计流程

【ICer必备 3】模拟IC设计全流程 ------------------------------------------------文末附往期文章链接--------------------------------------前言一、IC封装设计过程二、常见IC封装类型三、常见封装特点四、封装设计常用软件五、EM仿真常用EDA&#xff08;1&#xff09;HFS…

apt命令概述,apt命令在Ubuntu16.04安装openjdk-7-jdk

apt是一条linux命令&#xff0c;适用于deb包管理式操作系统&#xff0c;主要用于自动从互联网的软件仓库中搜索、安装、升级、卸载软件或操作系统。deb包是Debian 软件包格式的文件扩展名。 翻译过来就是&#xff1a; apt是一个命令行包管理器&#xff0c;为 搜索和管理以及查询…

解决MySQL删除数据后自增主键ID不连贯问题

首先我们需要取消id的自增和主键 下列代码以water表中的id列为例 alter table watermodify id int not null;alter table waterdrop primary key;然后重新生成id列 set i0; update water set water.id(i:i1);下一步就是重新设置为主键自增 alter table wateradd primary key…

【JSP技术】web杂谈(2)之JSP是什么?

涉及知识点 什么是JSP&#xff0c;JSP的特点&#xff0c;JSP的未来趋势&#xff0c;JSP的应用范例。深入了解JSP技术。 原创于&#xff1a;CSDN博主-《拄杖盲学轻声码》&#xff0c;更多内容可去其主页关注下哈&#xff0c;不胜感激 文章目录 涉及知识点前言1.什么是JSP2&…

Webpack和Vite简单使用

目录 WebPack 介绍 基础使用 初始化使用 webpack.config.js文件 webpack开发服务器 vite 介绍 使用 使用vite创建vue框架项目 WebPack 介绍 当我们习惯了在node中编写代码的方式后&#xff0c;在回到前端编写html、css、js这些东西会感觉到各种的不便。比如: 不能放心…

九、ElasticSearch 运维 -集群维度

1. 查看集群健康 用于简单的判断集群的健康状态&#xff0c;集群内的分片的分配迁移情况。 GET _cluster/health-------------------------Respond----------------------------- {"cluster_name" : "test-jie","status" : "green",…

使用数组的方式计算---任意给出一个年,月,日,判断出这是一年的第几天

任意给出一个年&#xff0c;月&#xff0c;日&#xff0c;判断出这是一年的第几天&#xff1b; 闰年算法&#xff1a;能被4整除且不能被100整除&#xff0c;或者能被400整除 如2015年 5 10 是这一年的第131天 使用数组的方式计算&#xff0c;将每个月的天数放在一个数…

蜂网互联 企业级路由器v4.31 密码泄露漏洞

漏洞描述 蜂网互联企业级路由器v4.31存在接口未授权访问&#xff0c;导致攻击者可以是通过此漏洞得到路由器账号密码接管路由器 漏洞影响 蜂网互联企业级路由器v4.31 网络测绘 app“蜂网互联-互联企业级路由器” 漏洞复现 payload http://ip:port/action/usermanager.ht…

c++ 杂食记

1. inline关键字 在C中&#xff0c;inline关键字用于指定函数应该被内联。 当一个函数被内联时&#xff0c;它的代码将直接插入到调用该函数的代码中&#xff0c;而不是作为单独的函数调用 这可以提高程序的性能&#xff0c;因为它减少了函数调用的开销&#xff0c;并提高了数…

计算机网络那些事之 MTU 篇

哈喽大家好&#xff0c;我是咸鱼 今天我们来聊聊计算机网络中的 MTU &#xff08;Maximum Transmission Unit&#xff09; 什么是 MTU ? MTU&#xff08;Maximum Transmission Unit&#xff09;是指数据链路层中的最大传输单元 通俗点来讲&#xff0c;MTU 是指数据链路层能…

基于workerman 即时通讯聊天(uniapp + pc)

laychat workerman 实现 webIM即时通讯系统 下载 laychat-master.zip https://github.com/hszyh/laychat 实现了功能: 1、通过snake后台实现对聊天成员的增删改查&#xff0c;动态推送给在线的用户 2、实现了群组的查找 3、实现了创建我的群组,删除我的群组,添加群组成员…

性能测试工具——LoadRunner内部介绍以及常见问题

目录 Tools Recording Options General Options 注释脚本 Review log Runtime-Settings General Network Browser Internet Protocol HTTPS证书 总结&#xff1a; Tools Recording Options 接下来我们挨个看一下里面的东东以及区别 General&#xff08;通常的&am…