什么是CAS?
compare and swap:比较和交换
那java什么地方需要这个呢?先介绍下几个概念性的东西
原子性是什么?
原子性就是最小单位,原子性操作就是最小的操作,无法再进一步分割,这操作的结果只有2种,完成或者未完成。
例如 int a = 1,这种就是原子性操作。
而a++,这个就不是原子性操作,因为a++,会拆解为3步,读取a的值,a增加1,再把a的值刷新。
还是那个例子,2个线程,同时对一个变量a进行递增,每次+1,递增100次。当2个线程都执行完毕,a的结果不会是200,会小于200。
为什么会这样?
其实这就是上面说的原子性问题,一开始a=0,线程A启动,执行a++,但是a++总共有3步,当线程A读取a的值(此时为0),然后递增1(a值为1),这是线程B启动,读取a值,由于线程A的a值尚未刷新回去,所以线程B获取到的值依然是0,而不是1,最终执行完毕的结果,就已经不是我们想要的结果了。
解决办法有哪些?
根据上面的例子,多线程处理资源嘛,肯定第一时间就会想到加个锁,因为锁能保证一段代码块执行完毕,从而保证原子性。
但是,如果要执行的代码足够简单的时候,例如上面的只有个a++,使用锁的话,会十分浪费,性能也不高,因为一个线程获得了锁,其他线程都无法进行了。
所以,就需要用到了CAS
CAS其实就是实现一种乐观的自旋锁,相较于synchronize的悲观锁,没有阻塞状态,自然也就没有上下文的切换了,而线程状态的切换,耗时比CAS的自旋比较操作耗时大得多。
CAS的缺点
①ABA问题
简单地描述,就是值被中途修改过的问题。
变量a=0
线程A:执行a++
线程B:执行a=10,然后再a=0
有一种情况,当线程A已经读取了a的值(0),然后线程B执行,a值先变为了10,后变回0,之后线程A再CAS,由于和预期一致,所以能赋值成功。
这种就是CAS的ABA问题,当然,如果我们不关心a值是否被改动过,只关心结果,那其实也无所谓。
解决方法就是加个版本戳。
②开销问题
成也自旋,败也自旋。当资源竞争足够大,成千上亿个线程去同时争一个资源,那其实大部分时候,大部分的线程都是在compare和重新获取的过程中,就会造成CPU的繁忙。
③只能保证一个共享变量的原子性
CAS的机制就是比较并且交换,只是针对一个资源。如果同时要保证多个资源的原子性,就不适合了,这时候就要用synchronize锁了。
那CAS用在什么地方?
其实JAVA的api包已经帮我们实现了很多了,我们只要拿来用就行,就是atomic的包,例如automicInteger。
调用过程
可以看到,最终就是调用compareAndSwapInt方法。它会用var1和var2先获取内存中AtomicInteger中的值,然后和传入的,之前获取的值var5做一下比较,也就是比较当前内存的值和预期的值是否一致,如果一致就修改为var5 + var4,否则就继续循环,再次获取AtomicInteger中的值,再进行比较并交换,直至成功交换为止。
我们假设两个线程交替运行的情况,看看它是怎样工作的:
初始AtomicInteger的值为0
线程A执行:var5 = this.getIntVolatile(var1,var2);获取的结果为:0
线程A被暂停
线程B执行:var5 = this.getIntVolatile(var1,var2);获取的结果为:0
线程B执行:this.compareAndSwapInt(var1,var2,var5,var5 + var4)
线程B成功将AtomicInteger中的值改为1
线程A恢复运行,执行:this.compareAndSwapInt(var1,var2,var5,var5 + var4),此时线程A使用var1和var2从AtomicInteger中获取的值为:1,而传入的var5为0,比较失败,返回false,继续循环。
线程A执行:var5 = this.getIntVolatile(var1,var2);获取的结果为:1
线程A执行:this.compareAndSwapInt(var1,var2,var5,var5 + var4),此时线程A使用var1和var2从AtomicInteger中获取的值为:1,而传入的var5为1,比较成功,将其修改为var5 + var4,也就是2,将AtomicInteger中的值改为2,结束。
网友评论