[JavaEE] CAS 介绍

前言:本文将简单介绍一下CAS的工作原理,功能以及使用

目录

[一,CAS 是什么?](#一,CAS 是什么?)

二,CAS的功能

(一),比较和交换

(二),保证多线程对单一变量的并发修改安全性

(三)CAS的局限性

1.ABA问题

如何解决?

2.高并发带来的反复比较

如何解决?

3.只能局限于单一变量的原子修改

如何解决?

[三,CAS 的使用](#三,CAS 的使用)


一,CAS 是什么?

CAS翻译为Compare and swap ,也就是比较和交换。CAS本质上是cpu的一条指令,既然是一条指令,也就是说明cas这个比较和交换的操作是原子性 的。

原子性的交换操作,可以在一些特殊场景下使用cas解决一些线程安全。如何使用CAS?其原理是什么?下面来展开叙述

二,CAS的功能

(一),比较和交换

比较和交换一定要有比较的对象和交换的对象值。CAS操作总共涉及到三个对象的值,分别是内存地址中的值(address value ),

比较的旧值(expected value) 和交换值(swap value)。其中旧值和交换值都存储在cpu的寄存器中

这个三个值之间的操作可以用下面的一个伪代码来简单演示说明

举一个形象点的例子叭~ 就好比一个银行柜员古法记账的场景,有一个柜员针对一个用户的余额进行统计;假设初始余额address是100元,用户给卡里面打了50元,这个时候按逻辑来说应该吧余额更新为150元。假设此时没有其他用户操作这个账户,也就只是单线程访问单一变量,这个时候就会进行CAS操作,address地址值为100,expected旧值是100,swap交换值为150。此时柜员就会记账更新前进行比较,比较address值和expected 的值是否都是100,如果是,那就把寄存器中的swap值换给address的值(ps:交换的目的是为了给address赋值,至于寄存器里面交换之后的值如何处理不必关心),此时就会完成余额从100变为150的记账操作。

(二),保证多线程对单一变量的并发修改安全性

CAS一般是在无锁,轻量级锁,自旋锁这几个常见的策略中被广泛使用的操作,既然被用于锁策略,那就可以一定程度上解决线程安全问题,如何解决的呢?

假如此时用户的老婆(线程2)在他存50元的时候,从卡里面取走了70元,此时余额address变为30.这个时候线程安全问题就出现了(同一时间多个线程修改同一变量),但是由于CAS操作的存在。

不会延续上下文的100进行+50操作(没有上下文切换),而是进行比较。这不比较倒还好,一比较就会发现address的值变了!被用户老婆的取钱操作使得余额变为了30 ,和expected值不同,导致return false,重新比较操作。同时把两个寄存器中expected_value 和 swap_value 做出更新;重复上面的比较操作。

可以看到CAS以这种比较和交换(Compare and swap )的巧妙操作,规避了线程安全问题的出现。

但是CAS之所以只能在特定场景下被使用,也不是没有道理的。要是所以的线程安全问题都能用CAS解决,那锁也就没有存在的必要了;这说明CAS也是有一定的局限性的。

(三)CAS的局限性

1.ABA问题

什么是ABA问题呢?这个很好理解,就是出现了多线程共同修改同一变量,但是线程2的修改操作是ABA的。线程2改了没有?改了!但是值没变!这就会导致线程1进行CAS操作在compare操作时感知不到线程2的修改操作。

继续使用上文的柜员记账操作,就好比用户1给银行卡转钱,与此同时用户1的老婆先从卡里取了70元,把余额修改为30,随后又向卡里打了70元,余额变回100。从address的值来看,对线程2的操作使得它的值从100 - 30 -100.也就是ABA修改。虽然出现了多线程并发修改同一变量,但是使用compare比较时无法发现其他线程的修改。

要注意虽然值没变,但是仍然会有数据安全隐患。当操作的数据结构是树或者链表这种复杂的数据结构时,ABA问题可能会导致数据错乱。

如何解决?

使用版本号(Version)就可以很好的解决这个ABA问题。什么是版本号?就是在compare比较的同时额外比较数据的版本。可以类似于一个时间戳的标记。每一个线程对共享变量的修改都会附带一个时间戳。

这样 一来即使线程2使用ABA修改了变量,但是线程1在compare比较时就会发现版本号(Version)不同,返回false,直到两者都相同才swap ,return true 更新内存的值。

2.高并发带来的反复比较

假如在高并发的场景下,同一时间就会出现很多的线程共同修改同一变量。假如线程1通过使用CAS来比较修改变量,在他比较的过程中,就可能会出现很多的其他线程也进行了修改操作,由于一直比较的结果都不同return false。所以CAS会反复的进入循环比较。比较?(不对)------再次比较(还是不对)------再次比较.......类似这样的循环。

这样的循环结果也就会导致线程的空转(自旋开销),给CPU带来巨大的开销负担

如何解决?

针对高并发写场景,可以使用 LongAdder(JDK 8 引入),它通过分散热点数据(Cell 数组)来减少竞争。在高并发场景下,LongAdder会直接初始化一个cell数组,每一个线程通过一个哈希值来映射到一个单一的cell槽位,这样其他线程就不再针对之前的一个变量争着去修改,而是转为只去自己的槽位坐修改操作。最后在调用sum把每一个槽位的修改操作累加在一起,把最终的修改和传给base单一变量。通过这种方法减少了线程之间对共享变量的竞争。

好比大家去排队买票,商场需要统计总票数,人多了就排的慢了(反复自旋),这时候就另开几个售票窗口(cell数组),这样其他人就通过哈希值去对应的窗口(cell槽位)去买票,减少竞争压力。最终统计票数只需要把所有窗口的售票记录拿到,累加在一起即可。

总而言之,LongAdder的核心功能是实现分段累加,空间换时间

3.只能局限于单一变量的原子修改

CAS的比较只能针对一个address的值进行比较,假如操作设计到多个变量,CAS就做不到原子修改两个变量了(毕竟不能同时compare多个变量)。

如何解决?

很简单,加锁即可。CAS指令一般用于处理简单的低竞争操作,当操作复杂时。直接使用synchronized保证操作的原子性即可。毕竟虽然CAS性能高,吞吐量大,但是保证不了数据安全的话我们还是直接使用加锁操作吧。

三,CAS 的使用

CAS是CPU的原子指令,提供给操作系统,操作系统对其封装。不过我们一般不使用操作系统封装的api(不好用),我们一般使用JVM进一步对其封装的原子类atomic class来实现多线程下对单一变量的原子修改

可以看到java为我们提供了很多种原子类供我们使用,可以实现对一个变量的原子修改操作

下面是一个简单使用AtomicInteger来多线程修改同一变量的操作

java 复制代码
import java.util.concurrent.atomic.AtomicInteger;

//TIP To <b>Run</b> code, press <shortcut actionId="Run"/> or
// click the <icon src="AllIcons.Actions.Execute"/> icon in the gutter.
public class Main {
    private static AtomicInteger count = new AtomicInteger(0);//初始原子Integer值为0
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(()->{
           for(int i = 0;i < 50000;i++){
               count.incrementAndGet();//实现count自增50000次
           }
        });
        Thread t2 = new Thread(()->{
            for(int i = 0;i < 50000;i++){
                count.incrementAndGet();//实现count自增50000次
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(count);
    }
}

注意:对于原子类的运算操作和基本数据类型的修改操作不同,而是要调用创建原子对象的成员方法。也就是不能使用count++来实现对count的自增操作。

运行结果如下:程序正常累加了10w次

可以看到使用原子类Atomic class,使用在不加锁 的情况下保证线程安全。有关其他原子类的使用不妨自己下去尝试一下~~

有关CAS的介绍就到这里结束了,如有纰漏,还请大佬们即使指出~~

相关推荐
zore_c2 小时前
【数据结构】栈——超详解!!!(包含栈的实现)
c语言·开发语言·数据结构·经验分享·笔记·算法·链表
lkbhua莱克瓦242 小时前
IO练习——登入注册
java·开发语言·io流·java练习题
running up2 小时前
Spring-AOP与代理模式
java·spring·代理模式
Reuuse2 小时前
登录突然失效:Axios 拦截器判空、localStorage 脏数据与环境变量踩坑
开发语言·前端
Seven972 小时前
递归与分治算法
java
风月歌2 小时前
小程序项目之基于微信小程序的高校课堂教学管理系统源代码(源码+文档)
java·微信小程序·小程序·毕业设计·源码
月明长歌2 小时前
【码道初阶】【Leetcode105&106】用遍历序列还原二叉树:前序+中序、后序+中序的统一套路与“先建哪边”的坑
java·开发语言·数据结构·算法·leetcode·二叉树
就玩一会_2 小时前
医疗挂号小程序
java
Oliver_LaVine2 小时前
java后端实现全链路日志ID记录
java·开发语言·spring