首页 > 其他分享 >CAS 概述

CAS 概述

时间:2023-04-14 15:24:28浏览次数:38  
标签:操作数 变量 CAS int race 线程 概述

一、案例

在介绍 CAS 之前先看一段代码

/**
 * @Author summer
 * @Description
 * @CreateDate 2023-04-13 15:58
 */
@Slf4j
public class VolatileDemo {
    // 定义 volatile 变量保证可见性、禁止指令重排
    private volatile static int race;
    // 定义线程数组个数
    private static final int THREAD_COUNT = 20;
    // 定义循环次数
    private static final int CIRCLE_COUNT = 1000;

    public static void increase() {
        race++;
    }

    public static void main(String[] args) throws InterruptedException {
        Thread[] threadGroup = new Thread[THREAD_COUNT];
        for (int i = 0; i < threadGroup.length; i++) {
            // 初始化线程组的每一个线程元素
            threadGroup[i] = new Thread(() -> {
                for (int j = 0; j < CIRCLE_COUNT; j++) {
                    increase();
                }
            });
            threadGroup[i].start();
        }
        // 主线程等待线程组中的每个线程执行完任务
        for (int i = 0; i < threadGroup.length; i++) {
            threadGroup[i].join();
        }
        log.info("race: {}", race);
    }
}

测试结果

20 个线程,每个线程执行 1000 次 race++ 操作,预期值应该是 20000,但是经过多次测试, race 都是一个小于 20000 的数值(有极小概率等于 20000)

这个时候大家可能有疑惑,race 变量使用了 volatile 关键字修饰了呀,为什么结果总是不符合预期呢?

volatile 的语义可以保证可见性、禁止指令重排,但是它不能保证原子性

看一下 increase() 方法的字节码指令

public static void increase();
descriptor: ()V
flags: ACC_PUBLIC, ACC_STATIC
Code:
  stack=2, locals=0, args_size=0
	 0: getstatic     #2 // 将静态变量 race 的值压入操作数栈顶
	 3: iconst_1         // 将常量值 1 压入操作数栈的栈顶,此时 race 的值会从栈顶的位置变成次栈顶
	 4: iadd			 // 将操作数栈栈顶的操作数 1 和 次栈顶的操作数(race 的值) 弹出操作数栈,并进行相加操作,把相加得到的结果重新存放在操作数栈的栈顶位置
	 5: putstatic     #2 // 将操作数栈栈顶的元素弹出操作数栈,刷新到主内存中
	 8: return
  LineNumberTable:
	line 20: 0
	line 21: 8

在代码层面 race++ 只有一行,但是对应的字节码指令却有 4 条,假设各个线程的执行顺序如下

 时间 线程 1   线程 2
t1  从内存中获取 race 的值(此时 race 为 0)  
t2    从内存中获取 race 的值(此时 race 为 0)
t3   将 race 的值压入操作数栈栈顶  
t4     将 race 的值压入操作数栈栈顶
t5   将常量 1 压入操作数栈栈顶  
t6     将常量 1 压入操作数栈栈顶
t7   将操作数栈栈顶和次栈顶元素弹出操作数栈并执行相加操作  
 t8    将操作数栈栈顶和次栈顶元素弹出操作数栈并执行相加操作
 t9  将得到的结果 1 刷新到主内存  
t10   将得到的结果 1 刷新到主内存

从上面的结果可以看出,两个线程执行两次 race++ 操作,预期的结果是 2,但是最终的结果是 1,导致我们不能获取到正确结果的原因是 race++ 不是原子性操作

那么如何解决呢,我们首先想到的是使用 synchronized 来修饰 increase() 方法,使整个 race++ 操作保证原子性,但是 synchronized 是基于线程阻塞来实现的,在并发场景下会有大量的 CPU 上下文切换,性能较差

那有没有比较好的解决方案呢,答案是有的,这就是我们下面要说的 CAS

 

二、CAS 概述

CAS(Compare And Swap) 即比较相同并交换,它是一条 CPU 并发原语,用于判断内存中某个位置的值是否与预期值相等,如果相等则将原来的值更改为新的值,如果不相等则不进行任何操作,由于整个过程是通过原语来实现的,具有原子性,不可被中断,并且整个过程是比较高效的,因为不会涉及到锁冲突与线程等待等问题

CAS 操作过程中包含三个运算值

V: 要读写的变量内存地址

A: 旧的预估值

B: 准备要设置成的新值

如果 V 这个内存地址上存放的变量的值等于旧的预估值 A,那么返回 true,然后把 V 这个内存地址上的值重新设置为新值 B

如果 V 这个内存地址上存放的变量的值不等于旧的预估值 A,那么返回 false

整个比较并替换的操作是一个原子性操作(由 CPU 并发原语保证)

其实 CAS 底层调用的是操作系统提供的 Atomic::cmpxchg 函数来实现功能的,该函数会根据当前系统是否是多处理器系统,如果是多处理器系统,那么将会添加 lock 前缀

 

三、CAS 使用场景

1、原子类

// AtomicInteger 原子类中方法
public final int getAndIncrement() {
	// this 为 AtomicInteger 的对象实例,valueOffset 为内存地址偏移量
	return unsafe.getAndAddInt(this, valueOffset, 1);
}

// Unsafe 类中方法
public final int getAndAddInt(Object var1, long var2, int var4) {
	int var5;
	do {
		// 根据原子类的对象实例地址 var1 以及内存地址偏移量 var2 获取到变量值
		var5 = this.getIntVolatile(var1, var2);
		// 1、根据对象内存地址 var1、偏移量 var2 重新获取一次变量的值
		// 2、如果该值与上一次获取的变量值 var5 相同,则进行加 1 操作,退出循环
		// 3、如果该值与上一次获取的变量值 var5 不同,则重新循环执行一次上述步骤
	} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

	return var5;
}

2、自旋锁

 

四、CAS 优缺点

CAS 优点

既然是优点,那么可以从 synchronized 的缺点来反推出 CAS 的优势,下面就来说一下使用 synchronized 有如下缺点

1、synchronized 是基于线程阻塞来实现的,当某个线程试图访问共享资源时,如果临界资源被其它线程获取(其它线程获取到锁),当前线程只能阻塞等待,直到其它线程释放锁为止

2、对于线程频繁而短暂持有锁的场景,CPU 将会消耗大量时间进行上下文切换,而真正用在控制和运算的时间很少,浪费 CPU 性能

而 CAS 则更加的灵活与轻量,对于执行很短的任务来说,CPU 只需要多次自旋就可以获取到锁,从而进行可靠的操作,整个过程不会有锁冲突、线程阻塞等待的问题

CAS 缺点:

1、只能保证一个共享变量的原子操作

传统 CAS 只能对一个变量保证原子操作,而对多个变量则要使用锁

解决此类问题,可以通过把多个共享变量合并成一个共享变量来操作,如 i = 2,j = a,可以合并为 ij = 2a,ij 既可用 CAS 来操作,也可以使用 jdk 提供的 AtomicReference 工具

2、循环开销大

CAS 通常是配合无限循环来一起使用的,我们可以看到 getAndAddInt 方法执行时,如果 CAS 失败,会一直进行尝试,如果 CAS 长时间一直不成功,可能会给 CPU 带来很大的开销

注意: CAS 适用于多核 CPU、任务执行时间短、竞争不激烈的场景

3、ABA 问题

             

假设 t1 时间节点我们观测到变量 A 的值是 3,t4 时间节点观测到变量 A 的值也是 3,我们无法确定变量 A 的值是否曾经发生过变化(也就是不能确定是上面两种情况的哪一种),这样便会导致一些问题,下面以银行转账为例

时间节点 线程 1 线程 2 线程 3
t1 查询余额 1000    
t2   查询余额 1000  
t3     查询余额 1000
t4 CAS 比较余额 1000 成功,扣款 300,余额 700    
t5   CAS 比较余额 1000 失败,余额 700  
t6 外部账户转入 300    
t7     CAS 比较余额 1000 成功,扣款 300,余额 700

从上面的例子中可以看出,实际余额 1000,外部转入 300 之后,线程 3 还是可以扣款成功,这就是典型的由于 ABA 问题导致的重复扣款

那么怎么解决 ABA 问题呢

Java 并发包为了解决这个问题,提供了一个带有标记的原子引用类 AtomicStampedReference,它可以通过控制变量值的版本号来保证 CAS 的正确性,因此在使用 CAS 前要考虑清楚 ABA 问题是否会影响程序并发的正确性,如果需要解决 ABA 问题,改用传统的互斥同步可能比原子类更加高效

 

 

 

标签:操作数,变量,CAS,int,race,线程,概述
From: https://www.cnblogs.com/xiaomaomao/p/17284536.html

相关文章

  • 4.1 网络层概述
    一、网络层提供的功能  1、从发送方主机传输报文段到接收方主机。发送方主机封装报文段(segments)为数据报(datagrams),接收方主机递交报文段给传输层。  2、在每个主机、路由器上都需要运行网络层协议。  3、路由器会检查通过它的所有IP数据报的头部字段,然后根据......
  • Java概述(简略)
    Java概述计算机程序:一系列有序指令的集合计算机语言发展史:机器语言-->汇编语言-->高级语言Java技术体系:JavaSE(JavaPlatform,StandardEdition标准版):允许您在桌面和服务器上开发和部署Java应用程序。Java提供了丰富的用户界面、性能、多功能性、可移植性和当今应用程序所需......
  • switch case语法
    switch(表达式)中表达式的值必须是一下六中类型:byteshortcharint枚举Stringswitchcase中case情况都一样的可以考虑合并switch(表达式){ case常量1: 语句1; break; case常量2: 语句2; break; ... default: break;}......
  • 6.case语句之场景示例:Rsync启动脚本
    1.编写一个rsync的启动停止脚本   实现:startstopstatusrestart   1.如何启动rsync        /usr/bin/rsync--daemon   2.如何停止rsync    pkillrsync #注意:千万不要使用rsync作为脚本的名字   3.参考系统中其他的服务的......
  • Linux Shell概述
     Linux系统的Shell作为操作系统的外壳,为用户提供使用操作系统的接口。它是命令语言、命令解释程序及程序设计语言的统称。Shell的分类:BourneShellCShellKornShellBourne-AgainShell Shell的基本功能:命令的解释执行环境变量的设置输入输出的重定向shell程序语言的设计Lin......
  • Django笔记二十三之case、when操作条件表达式搜索、更新等操作
    本文首发于公众号:Hunter后端原文链接:Django笔记二十三之条件表达式搜索、更新等操作这一篇笔记将介绍条件表达式,就是如何在model的使用中根据不同的条件筛选数据返回。这个操作类似于数据库中ifelifelse的逻辑。以下是本篇笔记的目录:model和数据准备When和Case......
  • oracle中case when 和 decode的区别
    1、decode简洁、casewhen复杂(但能实现的功能更多)2、返回值类型,casewhen的返回值类型必须一致如:SELECTCASEWHEN1=1THEN1ELSE'0'ENDFROMDUAL;--这样会报错:ORA-00932:数据类型不一致:应为NUMBER,但却获得CHAR   而decode的返回值类型需跟第一个返回值的类型一......
  • 概述
     一、概念GUI(GraphicalUserInterface)意为"图形用户接口"。Python中常用的三种GUI框架:tkinter、wxPython和pyQt。tkinter是开源的图形接口开发工具,原来是用TCL(ToolCommandLanguage,工具命令语言)编写的GUI函数库,现已移植到Python语言中,属于Python内建的模块,Python3版本中被称......
  • S7-1200 Modbus RTU 通信概述
    S7-1200ModbusRTU通信概述Modbus具有两种串行传输模式:分别为ASCII和RTU。Modbus是一种单主站的主从通信模式,Modbus网络上只能有一个主站存在,主站在Modbus网络上没有地址,每个从站必须有唯一的地址,从站的地址范围为0-247,其中0为广播地址,从站的实际地址范围为1-247。Modbus......
  • 【实践篇】基于CAS的单点登录实践之路
    作者:京东物流 赵勇萍前言上个月我负责的系统SSO升级,对接京东ERP系统,这也让我想起了之前我做过一个单点登录的项目。想来单点登录有很多实现方案,不过最主流的还是基于CAS的方案,所以我也就分享一下我的CAS实践之路。什么是单点登录单点登录的英文名叫做:SingleSignOn(简称SSO)。......