一、CAS的简单介绍
CAS的全称:“Compare And Swap”,字面意思是 “比较和交换”;一个CAS涉及到以下操作,有两个寄存器:A,SwapB,还有内存的值:AddressC。
先判断寄存器A是否和AddressC的值相同,相同,SwapB的值和AddressC的值进行交换,返回成功操作,否则返回失败操作。这里的交换值我们也可以理解成赋值操作,因为寄存器中的值我们不关心,用完就丢掉了,只关心内存中的值。
CAS逻辑(用伪代码来描述)
伪代码:
这里有两个寄存器:expectValue,swapValue,还有内存的值:address,初始化如图:
进入if语句,判断内存的值和寄存器e的值是否相等,如果相等,就交换寄存器swap和内存address的值,如图:
如图:
然后返回true,如果if条件不成立则返回false。
在计算机中,上述操作在计算机只是一条指令,因为单个指令的原因,所以CAS指令是原子的。
CAS指令不涉及到加锁,阻塞。基于CAS指令,合理使用的话,在多线程中,我们可以实现无锁编程;因为之前我们讨论并发编程的线程安全问题时,是通过加锁,阻塞这样方式解决线程安全问题,因为会有阻塞,所以性能也就会降低,用CAS指令实现无锁编程,也能保证线程安全,不涉及到阻塞,这样性能就能得到很大的提升,在多线程编程中打开了新世界的大门。
二、CAS在多线程中简单的使用
因为CAS是CPU的指令,有的cpu可能不支持CAS,但主流的CPU(x86,arm...)都是支持的。
CAS本身是CPU的指令,操作系统对其做了封装,jvm又对操作系统提供的api又做了一层封装。java中CAS的api是放在unsafe中的,这个包名的意思,顾名思义也是“不安全”的意思,一般在java中不建议使用,java的标准库中,对于CAS进行了进一步的封装,把CAS的一些操作封装成工具类,供程序猿使用。而主要的一个工具,叫做 “原子类”。
在 java.util.concurrent.atomic 包下,里面的类就是基于上述方式实现的,典型的类就是AtomicInteger类,里面有很多方法可以实现数值的自增、自减,以及基本的加减操作。下面的代码案例也是使用AtomicInteger展示。
我们以前写过一个代码,让两个线程实现一个变量自增10_0000次,如下代码是线程不安全。
public class AtomicIntegerTest1 { public static int count = 0; public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(() -> { for (int i = 0; i < 50000; i++) { count++; } }); Thread t2 = new Thread(() -> { for (int i = 0; i < 50000; i++) { count++; } }); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println(count); } }
执行结果:
输出结果也和我们预期结果不同,肯定是线程不安全的,而原因就是因为count++操作不是原子的,在计算机中有三个指令。
这里,我们使用CAS的方式,来实现让两个线程实现一个变量自增10_0000次,代码如下
public class AtomicIntegerTest1 { public static AtomicInteger count = new AtomicInteger(0); public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(() -> { for (int i = 0; i < 50000; i++) { count.getAndIncrement();//和count++意思一样 } }); Thread t2 = new Thread(() -> { for (int i = 0; i < 50000; i++) { count.getAndIncrement();//和count++意思一样 } }); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println(count); } }
执行结果:
因为这里用CAS的方式,原本count++操作在计算机中有3条指令,不是原子的,肯定线程不安全;但是用CAS的方式就可以把像count++这样的操作,用一条指令完成,是原子的,在这里就不涉及到线程安全问题了。
AtomicInteger中有很多方法:自增,自减,+=,-=等待,这里就不展开讨论了。
三、原子类自增的代码分析
代码还是上述的代码,如下:
public class AtomicIntegerTest1 { public static AtomicInteger count = new AtomicInteger(0); public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(() -> { for (int i = 0; i < 50000; i++) { count.getAndIncrement();//和count++意思一样 } }); Thread t2 = new Thread(() -> { for (int i = 0; i < 50000; i++) { count.getAndIncrement();//和count++意思一样 } }); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println(count); } }
在标准库中,getAndIncrement方法是这样的:
这里比较难理解,我们用伪代码的形式介绍,伪代码如下:
当有两个线程对同一个变量进行自增操作时,执行流程是这样的:
这里的oldValue是寄存器中的值,它的值就是AtomicInteger括号里面初始化的值,value是内存中的值。从上到下,把内存value的值都赋值给oldValue,两个线程拿到的都是同一个内存value的值。
这时,右边的线程while循环里的判断,先执行CAS这里的操作,如图:
执行完CAS操作后返回true,然后while循环里true != ture,条件不成立,不执行while循环内的代码。
当左边线程进入while循环里面的判断语句时,如图:
也会进入CAS操作,这里因为内存value的值修改了,当前线程寄存器oldValue值还是0,给oldValue+1,会返回false,这时循环条件false != true成立,就会执行oldValue = value操作,如图:
再次进入循环条件里面执行CAS操作,这时候value == oldValue,所以会让oldvalue+1赋值给value,这时候value的值就是2了,如图:
所以,两个线程不管顺序是啥样的,使用getAndIncrement方法都不会出现线程安全问题,因为CAS操作本身就是原子的,原因的逻辑理解也大概是下面这样的:
如果cas不成功,会重复上面的操作,再次读取数据,这次读取到的数据就是正确的了,cas也就能成功。意思就是这个方法里面会判断拿到的值是不是最新值,如果不是就去拿最新的值,再去CAS,这时候因为拿到的是最新值,所以这时能CAS成功。
四、CAS的ABA问题
举个栗子:我们现实生活中有无良商家会贩卖翻新机,而翻新机是较低价格回收的二手机,进行一系列翻新,变成和新机一样的外表,肉眼看不出来,但本质还是翻新机。
(1)CAS的ABA问题
而CAS中的ABA问题类似翻新机,CAS在使用的时候,主要关心的是内存上的值是否和CPU上寄存器的值相同,如果相同才进行对内存的赋值操作(交换另一个寄存器的值),不相同就不交换。如果一个线程在CAS操作时,穿插了其他线程的操作,内存上的值变换了两次:0 -> 100, 100 -> 0,内存中的值本质上是没改变的,但是它有改变的操作,进行CAS操作是感知不到内存的值是否有改变的;一般情况下,出现了上述情况,也不会出啥问题,不会产生BUG,但也有非常极端的情况,如下:
在ATM取钱的场景,你的银行卡里有1000块,要取500块,在点击取500块的时候卡了,没有反应,你又点了一下,总共点了两下,这时候就会产生两个线程,这里如果没有其他的一些极端情况,不会出问题,如下是伪代码进行分析:
但如果在右边线程的CAS操作完后,这里又有其他线程给balance变量加500,又变成了1000,就会出现问题,如图:
这样,就会导致,你存了500进去,但钱还是500,而能取出的钱也只有500,这个BUG就非常的严重,造成了吞钱的现象。这也就是ABA问题。
(2)解决ABA问题的方案
1、约定数据变化只能是单向的(只能增加或只能减少),不能是双向的(既能增加又能减少)。
2、对于本身就必须双向变化的数据,引入版本号,这个版本号的数字只能增加,不能减少
注意:上述思路不局限于CAS本身,有一些场景没有使用CAS,也可能会产生上述问题,但也可以使用上面的思路。