一、什么是CAS
前面两篇文章提到CAS操作,那么CAS操作到底是什么东西呢?今天我们来了解一下CAS机制
CAS(Compare-And-Swap),它是一条CPU并发原语,用于判断内存中某个位置的值是否为预期值,如果是则更改为新的值,这个过程是原子的。CAS是一种系统原语,Java中利用原子操作类实现,指的是java.util.concurrent.atomic包下,一系列以Atomic开头的包装类。如AtomicBoolean,AtomicUInteger,AtomicLong。它们分别用于Boolean,Integer,Long类型的原子性操作。
我们来分析两段代码更好地理解一下CAS机制:
代码如下:
package CAS;
public class threadeg {
public static int count=0;
public static void main(String[] args) {
// TODO Auto-generated method stub
for(int i=0;i<2;i++) {
new Thread(
new Runnable() {
public void run() {
try {
Thread.sleep(10);
}catch(InterruptedException e){
e.printStackTrace();
}
for(int j=0;j<10;j++) {//每个线程对count自增20
count++;
}
}
}).start();
}
try {
Thread.sleep(1000);
}catch(InterruptedException e){
e.printStackTrace();
}
System.out.println("count="+count);
}
}
上面例子运行出来的count结果不能保证是20,因为这段代码是非线程安全的。他没办法保证我们最后运行出来的结果是20。
接下来我们用AtomicInteger类实现:
package CAS;
import java.util.concurrent.atomic.AtomicInteger;
public class threadeg2 {
public static AtomicInteger count=new AtomicInteger(0);
public static void main(String[] args) {
// TODO Auto-generated method stub
for(int i=0;i<2;i++) {
new Thread(
new Runnable() {
public void run() {
try {
Thread.sleep(10);
}catch(InterruptedException e){
e.printStackTrace();
}
for(int j=0;j<20;j++) {//每个线程对count自增20
count.incrementAndGet();
}
}
}).start();
}
try {
Thread.sleep(1000);
}catch(InterruptedException e){
e.printStackTrace();
}
System.out.println("count="+count);
}
}
使用AtomicInteger之后,最终的输出结果可以保证是20。有人说可以用synchronized关键字来保证线程安全。但是使用AtomicInterger类在某种程度上会比synchronized关键字好,因为synchronized关键字对于没有竞争到锁资源的线程会进行阻塞处理,这时候发生用户态与内核态之间的转换,十分消耗资源,代价较大。尽管后面的对synchronized进行了优化,但是线程一多还是存在该问题。而Atomic操作类的底层正是用到了“CAS机制”。
CAS叫做CompareAndSwap,比较并交换,主要是通过处理器的指令来保证操作的原子性,它包含三个操作数:
1、变量内存地址,V表示
2、旧的预期值,A表示
3、准备设置的新值,B表示
当执行CAS指令时,只有当V等于A时,才会用B去更新V的值,否则就不会执行更新操作。
当多个线程使用CAS操作一个变量时,只有一个线程会成功,并成功更新变量值,其他线程均会失败。失败线程会重新尝试或将线程挂起(阻塞)
二、CAS的三大缺点
CAS的缺点主要有3点:
(1)ABA问题:ABA的问题指的是在CAS更新的过程中,当读取到的值是A,然后准备赋值的时候仍然是A,但是实际上有可能A的值被改成了B,然后又被改回了A,这个CAS更新的漏洞就叫做ABA。只是ABA的问题大部分场景下都不影响并发的最终效果。Java中有AtomicStampedReference来解决这个问题,他加入了预期标志和更新后标志两个字段,更新时不光检查值,还要检查当前的标志是否等于预期标志,全部相等的话才会更新。
(2)循环时间长开销大:自旋CAS的方式如果长时间不成功,会给CPU带来很大的开销。(不知道自旋是什么的可以看我写的关于锁的文章。)
(3)只能保证一个共享变量的原子操作:只对一个共享变量操作可以保证原子性,但是多个则不行,多个可以通过AtomicReference来处理或者使用锁synchronized实现。
ABA问题
ABA问题空讲有点不明白所以我们举个例子。
假设,小童银行卡里有1000元。要用一个遵循CAS的提款机提款500元去买衣服。此时由于提款机硬件开小差,将小童的提款操作提交了两次,此时有两个线程,两个线程都是获取当前值1000元,期望更新值为500元。正常情况下,应该是两个线程一个成功一个失败,小童的余额被扣款一次
此时的状态:
此时假设线程1首先执行成功,而线程2由于某些原因阻塞。恰巧,小童的妈妈得知小童要买鞋,给小童汇款500元(此次操作为线程3)。
此时的状态为:
此时线程2仍在阻塞,所以提款机执行线程3。且由于CAS检测成功,所以线程3执行成功。
这时候线程2恢复运行,由于一开始线程2获取的当前值是1000,且现在的值也为1000,所以CAS检测成功,将值更改为500。
原本线程2应当提交失败,小童的银行卡余额应该保持为1000元,结果由于CAS里的ABA问题导致线程2提交成功了。所以小童很悲催的少掉了500块钱。
既然遇到了这个问题那么怎么解决ABA问题呢?
JDK的Atomic包里提供了一个类AtomicStampedRefernce来解决ABA问题。这个类的compareAndSet方法的作用是首先检查当前引用是否等于预期引用,并且检查标志stamped是否为预期标志,如果全部一致,则继续。
此篇文章为学习笔记,是对别人知识的理解,加上自己的一些个人理解汇聚而成。若有侵权联系删除。写作不易,望好兄弟们来个三连支持。感激不尽。
注:原文出处为https://mp.weixin.qq.com/s/-xFSHf7Gz3FUcafTJUIGWQ