什么是CAS

CAS的全称是Compare and Swap(比较和交换),是一种特殊的修改数据的方式,线程通过CAS修改数据时整个过程涉及到三个数据:要修改的内存数据V、执行CAS操作前读取V并将V的值复制到工作空间计作A(预期值)、修改后的数据B,执行CAS操作中当且仅当预期值A和内存值V相同时,将内存值V修改为B并返回true,否则视为修改失败返回false。
图片

Atomic对CAS的应用

Atomic包是Java.util.concurrent下的另一个专门为线程安全设计的Java包,包含多个原子操作类,我们以AtomicInteger为例看看java如何通过CAS实现原子性。

incrementAndGet方法,以原子方式将当前值增加1并返回增加后的值:

1
2
3
4
public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}

我们发现incrementAndGet方法把这个操作委托给unsafe类的getAndAddInt方法处理,我们继续看getAndAddInt方法源码:

1
2
3
4
5
6
7
8
9
10
11
12
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {

// 读取AtomicInteger在内存中对应的值,并复制一份赋值给var5,作为期望值
var5 = this.getIntVolatile(var1, var2);

// 将AtomicInteger对象引用、偏移量、预期值、修改后的值交给compareAndSwapInt也就是CAS方法循环执行,直到true
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

return var5;
}

代码中我们可以看到,真正的CAS修改操作是compareAndSwapInt方法,我们继续往下看:

1
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);

到这里的时候我们发现compareAndSwapInt方法是被native修饰的,说明接下来的代码是使用C++实现的了。源码就不贴了,这个方法实际上是利用处理器提供的汇编指令CMPXCHG。当CPU执行此修改指令时发现带有CMPXCHG前缀,那么会采用CAS方式(比较并交换操作数)修改数据,并且保证比较、交换俩个步骤不会被上下文切换打断。当且仅当预期值var4与要修改的内存值相等时,将内存值修改为var5。

如果你细心的话会发现,在多核CPU的操作系统中仅仅保证CAS的俩个步骤不被上下文切换打断没什么卵用,如果俩个线程并行同时对某个AtomicInteger(0)执行incrementAndGet方法,怎么保证高速缓存中取出的期望值不是脏数据?怎么保证多个处理器不会同时执行到CAS的比较操作并且都返回true,继而同时修改内存值为1,最终导致结果应该是2却因为线程安全问题变为1?

我们回头看看AtomicInteger的其他源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
...

private volatile int value;

public AtomicInteger(int initialValue) {
value = initialValue;
}

public AtomicInteger() {
}

public final int get() {
return value;
}

...

AtomicInteger内部有个int类型的value属性,代表着自身的值,并且AtomicInteger读写操作都是围绕这个值进行的,并且这个类被volatile修饰的。到这里思路就清晰了,volatile修饰符保证了value值的可见性,线程不会出现读到脏数据的情况。

对于第二种情况百度的资料很少提及,所以也无法确定CPU到底如何解决这个问题。但是我在知乎上看到了俩个感觉还算靠谱的答案。首先被volatile修饰的变量会使用MESI协议确保同一时刻只有一个处理器修改值,并且把其他处理器此值的缓存设为无效,当第二个处理器想要修改值时发现无效,CAS操作失败,返回false,另一个答案则表示当多个处理器同时使用cmpxchg指令(也就是CAS)操作同一个数据时,总线会进行仲裁只有一个处理器执行CAS,其他处理器连比较操作都不会执行,直到上一个处理器执行完毕后总线再次仲裁并选中自己。

第一种答案强调使用MESI的失效机制解决问题,第二种答案则强调将CAS视为一个整体,在执行比较操作的时候就会利用MESI协议将数据修改为M状态。不同的CPU架构可能解决问题的方式也不同,总之CPU保证多处理器并行执行CAS不会出错,Java保证volatile+自旋CAS修改数据的原子性,以后搞懂了再更新。

ABA问题

Java在1.5版本引进了AtomicStampedReference类,采用版本号的机制解决这个操蛋的问题。

总结

  • CAS是典型的乐观派操作,每次都迷之自信认为操作一定成功,但是在高并发比较严重的情况下会导致大量线程不断的循环,增大CPU的消耗。
  • CAS只能保证单个共享变量的原子操作,如果操作涉及多个共享变量,必须要排他锁解决
  • 仅仅依靠CAS无法保证原子性,必须配合CPU缓存锁一起保证。

评论