一、什么是CAS机制(compare and swap)
1.概述
CAS
的全称为Compare-And-Swap
,直译就是对比交换。是一条CPU的原子指令,其作用是让CPU先进行比较两个值是否相等,然后原子地更新某个位置的值。经过调查发现,其实现方式是基于硬件平台的汇编指令,就是说CAS是靠硬件实现的,JVM只是封装了汇编调用,那些AtomicInteger
类便是使用了这些封装后的接口。
简单解释:CAS操作需要输入两个数值,一个旧值(期望操作前的值)和一个新值,在操作期间先比较下在旧值有没有发生变化,如果没有发生变化,才交换成新值,发生了变化则不交换。
2.CAS算法的作用:
解决多线程条件下使用锁造成性能损耗问题的算法,保证了原子性,这个原子操作是由CPU来完成的
3.CAS的原理:
CAS算法有三个操作数,通过内存中的值(V
)、预期原始值(A
)、修改后的新值(B
)。
(1)如果内存中的值和预期原始值相等, 就将修改后的新值保存到内存中。
(2)如果内存中的值和预期原始值不相等,说明共享数据已经被修改,放弃已经所做的操作,然后重新执行刚才的操作,直到重试成功。
注意:
(1)预期原始值(A)是从偏移位置读取到三级缓存中让CPU处理的值,修改后的新值是预期原始值经CPU处理暂时存储在CPU的三级缓存中的值,而内存指定偏移位置中的原始值。
(2)比较从指定偏移位置读取到缓存的值与指定内存偏移位置的值是否相等,如果相等则修改指定内存偏移位置的值,这个操作是操作系统底层汇编的一个原子指令实现的,保证了原子性。
- JVM中CAS是通过
UnSafe
类来调用操作系统底层的CAS指令实现。 - CAS基于乐观锁思想来设计的,其不会引发阻塞,
synchronize
会导致阻塞。
import java.util.concurrent.atomic.AtomicInteger;
public class CASDemo {
public static void main(String[] args) {
AtomicInteger atomicInteger = new AtomicInteger(2023);
//期望时5,如果是5则改成2022
System.out.println(atomicInteger.compareAndSet(2023, 2022) + "\t" + atomicInteger.get());
System.out.println(atomicInteger.compareAndSet(2023, 2022) + "\t" + atomicInteger.get());
}
}
结果:
true 2022
false 2022
二、CAS和syncronized的比较
- CAS线程不会阻塞,线程一致自旋
syncronized
会阻塞线程,会进行线程的上下文切换,会由用户态切换到内核态,切换前需要保存用户态的上下文,而内核态恢复到用户态,又需要恢复保存的上下文,非常消耗资源。
三、CAS的缺点
(1)ABA问题
如果一个线程t1
正修改共享变量的值A,但还没修改,此时另一个线程t2
获取到CPU时间片,将共享变量的值A修改为B,然后又修改为A,此时线程t1
检查发现共享变量的值没有发生变化,但是实际上却变化了。
解决办法:
使用版本号,在变量前面追加上版本号,每次变量更新的时候把版本号加1
,那么A-B-A
就会变成1A-2B-3A
。从Java1.5开始JUC包里提供了一个类AtomicStampedReference
来解决ABA问题。AtomicStampedReference
类的compareAndSet
方法作用是首先检查当前引用是否等于预期引用,并且当前版本号是否等于预期版本号,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。
(2)CAS频繁失败导致CPU开销大
循环时间长开销会比较大:自旋重试时间,会给CPU带来非常大的执行开销
(3)只能支持一个变量的原子操作,不能保证整个代码块的原子操作
只能保证一个共享变量的原子操作,不能保证同时对多个变量的原子性操作
解决办法:
从Java1.5开始JDK提供了AtomicReference
类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行CAS操作
四、CAS使用注意事项
(1)CAS需要和volatile配合使用
CAS只能保证变量的原子性,不能保证变量的内存可见性。CAS获取共享变量的值时,需要和volatile
配合使用,来保证共享变量的可见性
(2)CAS适用于并发量不高、多核CPU的情况
CPU多核情况下可以同时执行,如果不合适就失败。而并发量过高,会导致自旋重试耗费大量的CPU资源