CAS

[TOC]

阻塞同步和非阻塞同步

阻塞同步需要线程阻塞和唤醒所带来的性能问题,它属于一种悲观的并发策略,无论共享数据是否真的会出现竞争,它都要进行加锁。

cmpxchg

CAS是什么

它是乐观并发策略:

1
**先进行操作,如果没有其它线程争用共享数据,那操作就成功了,否则不断地重试,直到成功。这种乐观的并发策略的许多实现都不需要将线程阻塞,因此这种同步操作称为非阻塞同步。**

CAS 表示 Compare And Swap,比较并交换,CAS 主要需要三个操作数,分别是内存中存放的实际值V、旧的预期值 A 和准备设置的新值 B。CAS 指令执行时,当且仅当 V 符合 A 时,处理器才会用 B 更新 V 的值,否则它就不执行更新。但不管是否更新都会返回 V 的旧值,这些处理过程是原子操作,执行期间不会被其他线程打断。

AtomicInteger

J.U.C 包里面的整数原子类 AtomicInteger 的方法调用了 Unsafe 类的 CAS 操作。
以下代码使用了 AtomicInteger 执行了自增的操作。

1
2
3
4
private AtomicInteger cnt = new AtomicInteger();
public void add() {
cnt.incrementAndGet();
}

以下代码是 incrementAndGet() 的源码,它调用了 Unsafe 的 getAndAddInt() 。

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

以下代码是 getAndAddInt() 源码,var1 指示对象内存地址,var2 指示该字段相对对象内存地址的偏移,var4 指示
操作需要加的数值,这里为 1。通过 getIntVolatile(var1, var2) 得到旧的预期值,通过调用 compareAndSwapInt()
来进行 CAS 比较,如果该字段内存地址中的值等于 var5,那么就更新内存地址为 var1+var2 的变量为 var5+var4。
可以看到 getAndAddInt() 在一个循环中进行,发生冲突的做法是不断的进行重试。

1
2
3
4
5
6
7
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;
}

ABA问题

1
2
3
4
如果一个变量初次读取的时候是 A 值,它的值被改成了 B,后来又被改回为 A,那 CAS 操作就会误认为它从来没有被改变过。
J.U.C 包提供了一个带有标记的原子引用类 AtomicStampedReference 来解决这个问题,它可以通过控制变量值的版
本来保证 CAS 的正确性。大部分情况下 ABA 问题不会影响程序并发的正确性,如果需要解决 ABA 问题,改用传统
的互斥同步可能会比原子类更高效。