前言:本文将简单介绍一下CAS的工作原理,功能以及使用
目录
[一,CAS 是什么?](#一,CAS 是什么?)
[三,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的介绍就到这里结束了,如有纰漏,还请大佬们即使指出~~