概念
- CAS:Compare and Swap,实现并发算法时常用到的一种技术。CAS操作包含三个操作数——内存位置、预期原值及新值。执行CAS操作的时候,将内存位置的值与预期原值比较,如果相匹配,那么处理器会自动将该位置值更新为新值,否则,处理器不做任何操作。CAS是一条CPU的原子指令(cmpxchg指令),不会造成所谓的数据不一致问题,Unsafe提供的CAS方法(如compareAndSwapXXX)底层实现即为CPU指令cmpxchg
为什么要使用CAS?
多线程访问同一共享资源时,会出现三个问题:可见性、有序性、原子性
1.同步锁机制
- 通过synchronized悲观锁获取对象的监视器,从而实现多个线程的互斥访问,也就是说,一旦有并发问题,实际上之后一个线程在进行操作,对于高并发并不具备优势
- 同步锁机制虽然能保证happen before原则,线程1早于线程2获取到 锁,那么线程1一定早于线程2完成操作,但是对于大多数并发场景并不需要 这个原则,只需要保证结果的一致性即可,CAS操作保证最小力度上具备happen before原则,可以大大提升性能
2.Volatile轻量同步机制
- 我们之前讲过Volatile关键字能够在并发条件下
- 强制将修改后的值刷新到主内存中来保持内存的可见性
- 通过 CPU内存屏障禁止编译器指令性重排来保证并发操作的有序性
- 但是如何保证原子操作?
就Volatile的内存可见性实现而言,很显然Volatile不能保证操作具有原子性,怎么解决原子性问题?CAS实现给了我们答案。
原理
- CAS的实现是乐观锁的一种,他认为认为线程的并发访问不会产生 冲突,所以操作过程并不会枷锁,在操作结束需要更新共享内存资源时需要判断下其他线程时候修改了这个资源,如果没被修改,就将自己线程的值刷新到主内存中,如果在要更新的时候发现主内存的数据被其他线程更新,就处于重试状态,直到没有冲突为止
- CAS包含三个值当前内存值(V)、预期原来的值(A)以及期待更新的值(B)。
- 如果内存位置V的值与预期原值A相匹配,那么处理器会自动将该位置值更新为新值B,返回true。否则处理器不做任何操作,返回false。
- 比如当前线程比较成功后,准备更新共享变量值的时候,这个共享变量值被其他线程更改了,那么CAS函数必须返回false。
- Java在实现这个操作时使用的Unsafe类来实现,提供了三个接口,入参obj用来获取初始对象的内存地址,valueOffset为内存地址的偏移值
public final native boolean compareAndSwapObject
(Object obj, long valueOffset, Object expect, Object update);
public final native boolean compareAndSwapInt
(Object obj, long valueOffset, int expect, int update);
public final native boolean compareAndSwapLong
(Object obj, long valueOffset, long expect, long update)
应用
JUC包下的AtomicInteger类
- AtomicInteger的 自增调用Unsafe类的CAS操作
/**
* Atomically increments by one the current value.
*
* @return the previous value
*/
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
// unsafe类下的 getAndAddInt实现
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
- 从实现上来看getAndAddInt有自旋操作,如果单次的CAS操作没有 成功,会继续进行CAS操作,CAS操作到了底层是靠CPU指令来实现的
CAS缺点
1. ABA问题
- CAS需要在操作值的时候检查下值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了
- 解决ABA问题思路:
- 每次变量更新时进行版本号追加工作,进行版本号和内容组合比较
- 检查当前比较的值得引用是是否相等
2.需要CAS操作的地方基本需要自旋,自旋时间 过长会带来CPU大的开销
3.CAS操作的粒度只是针对一个共享变量,针对多个共享变量的原子操作而言,应该需要用锁
引申
- Java锁机制
https://blog.csdn.net/weixin_41950473/article/details/92080488 - Java中的Volatile关键字
https://blog.csdn.net/weixin_41950473/article/details/91966135 - Java Synchronized锁
https://blog.csdn.net/weixin_41950473/article/details/90049998 - 什么是原子操作
“原子操作(atomic operation)是不需要synchronized”,原子操作是指不会被线程调度机制打断的操作;这种操作一旦开始,就一直运行到结束,中间不会有任何 context switch (切 换到另一个线程)。