Bootstrap

【Java里的CAS机制】什么是CAS,CAS机制


一、什么是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

;