什么是CAS?
CAS(Compare-and-Swap,比较并交换)操作是一种乐观的并发策略。
CAS 操作需要三个操作数 , 内存位置(V)、预期值(A)和新值(B)。执行CAS操作时,如果内存位置的值与预期值相等,处理器就会用新值(B)更新V的值。否则,处理器不做更新。无论是否更新了V的值,都会将V的旧值返回。
上面的比较并交换的过程,虽然从语义上来看是多次操作,但事实上,它的操作是原子操作,不会被打断。这是由硬件来保证的(利用CPU的CAS指令,同时使用JNI来完成非阻塞算法,相比于独占锁效率会更好)。比如X86可以通过cmpxchg指令来完成CAS操作。
CAS无锁算法和阻塞同步
java程序员接触最多的阻塞同步应该是synchronized 了,它是一种互斥的同步机制,同一时刻只能有一个线程进入临界区,只有当该线程从临界区出来,释放了互斥锁后其他线程才能进入。这是一种悲观的并发策略,无论共享数据是否出现竞争都要进行加锁。而线程的唤醒和阻塞是比较消耗性能的,因此synchronized 算是比较重的操作(当然JDK1.6后已经对synchronized 进行了很多优化,会从偏向锁逐渐膨胀到重量级锁,而不是上来就是重量级锁)。而CAS操作有别于synchronized 之处在于它不会阻塞线程,如果现在有多条线程对共享变量进行更新,只有一个线程会成功,其他线程都会失败,失败的线程会不断重试,直到成功。这也是为什么说CAS是一种乐观的并发策略的原因,CAS操作是先进行更新,如果没有其他线程对共享变量进行操作就成功。如果有就失败,失败了再采取补救措施(不断重试,直到成功)。
我们如何使用CAS操作
在JDK1.5后,java给我们提供了sun.misc.Unsafe类,这个类中的一系列方法提供了对CAS操作的支持,不可可惜的是我们不能直接使用它,只能间接的通过java提供的一系列Atom原子类来进行CAS操作,这些原子类内部使用的就是Unsafe类提供的CAS操作。
现在看看AtomicInteger如何实现CAS操作的。
Example:
public class Main {
static int a=0;
static AtomicInteger atomicInteger=new AtomicInteger(0);
static CountDownLatch latch=new CountDownLatch(3);
public static void main(String[] args) throws InterruptedException {
ExecutorService service= Executors.newCachedThreadPool();
for (int i=0;i<3;i++){
service.submit(new Runnable() {
@Override
public void run() {
for (int j=0;j<1000;j++){
a++;
atomicInteger.getAndIncrement();
}
latch.countDown();
}
});
}
latch.await();
System.out.println("a="+a);
System.out.println("atomicInteger="做了啥+atomicInteger.get());
}
}
上面代码演示了两个共享变量a、atomicInteger在多线程并发的情况下自增的结果。我们知道变量a的自增不是原子性的,并且对其他线程也不是立即可见的,因此结果肯定有问题。那atomicInteger呢?
a=2881
atomicInteger=3000
多次执行a的结果都不同,但是atomicInteger和我们预期结果一致,这是因为atomicInteger的自增内部使用了Unsafe类的CAS操作。点进入看看getAndIncrement方法做了啥?
AtomicInteger#getAndIncrement
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
可以看到内部确实调用了unsafe提供的方法。
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;
}
var2就是内存位置(V),var5是预期值,var5 + var4代表新值,在getAndAddInt方法中首先获取旧值作为预期值,然后调用compareAndSwapInt进行CAS操作,判断内存位置的值与预期值是否相等,如果相等就用新值更新V的值。如果不想等就重试,直到成功。
比如说现在有多个线程进入getAndAddInt方法,线程A,B获取到内存位置的旧值var5,但是A线程先一步调用了compareAndSwapInt方法更新了内存位置的值,线程B再调用compareAndSwapInt的时候就会发现内存位置的值和var5不相等了,说明有其他线程更新了,那自己只好重试了。
注意:“比较+更新”操作是封装在compareAndSwapInt()中,它是借助于一个CPU指令完成的,属于原子操作,文章开头已经解释过了。
CAS的不足
- CAS操作只能保证对一个共享变量操作的原子性,因此使用场景有限。
- 存在一个逻辑上的Bug,"ABA"问题。当有多个线程对共享变量操作,其中一个线程先将变量从A更新为B,然后又重新更新为A,其他线程进行CAS操作时会认为没有变化,实际上却变化了。当然大部分情况下"ABA"问题不会影响并发程序的正确执行。
- CAS操作如果用在复杂的并发场景容易出错,会增加程序的复杂性。
- 消耗CPU性能,CAS操作失败会不断的重试,虽然线程不会被挂起,但确是以消耗CPU为代价的,只不过比其阻塞唤醒的代价要小的多。