CAS的全称为CompareAndSet,比较并设置。
CAS可以说是并发编程的基石,很多实现并发的方式底层都是依赖于CAS操作。
CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。CAS的语义是“我认为V的值应该为A,如果是,那么将V的值更新为B,否则不修改并告诉V的值实际为多少”。
CAS是项乐观锁技术,当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。是一条CPU的原子指令,其实现方式是基于硬件平台的汇编指令,就是说CAS是靠硬件实现的。
cas是如何保证自己是原子操作的,它在底层是lock cmpxchg执行(x86架构)。当某个线程执行到lock指令时,cpu会让总线锁住,当这个核把指令执行完毕后,在开启总线。这个过程不会被线程的调度机制打断。所以是原子的。
cas操作需要与volatile配合使用,满足cas的类型会将共享变量设置为volatile的,因为volatile保证的变量的可见性,这样cas操作才可以获取到共享变量的最新值。与之前的锁比较,cas保证了原子性,配合volatile保证了可见性。
拿出AtomicInteger来研究在没有锁的情况下是如何做到数据正确性的。
private volatile int value;
在没有锁的机制下可能需要借助volatile原语,保证线程间的数据是可见的(共享的)。
这样才获取变量的值的时候才能直接读取。
public final int get() {
return value;
}
然后来看看++i是怎么做到的。
public final int incrementAndGet() {
for (;;) {//无限循环直到成功
int current = get();
int next = current + 1;
if (compareAndSet(current, next))//相同返回true,不同返回false
return next;
}
}
在这里采用了CAS操作,每次从内存中读取数据然后将此数据和+1后的结果进行CAS操作,如果成功就返回结果,否则重试直到成功为止。
而compareAndSet利用JNI来完成CPU指令的操作。
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
整体的过程就是这样子的,利用CPU的CAS指令,同时借助JNI来完成Java的非阻塞算法。其它原子操作都是利用类似的特性完成的。
这种以无锁方式保证临界变量正确性与有锁的效率比起来,无锁会更高一点。因为有锁的情况下,当线程没有获得到共享资源时,会进入阻塞状态,就会进行上下文切换,比如一辆车要进行停车,熄火,再启动。这样耗费的时间就会更长。但无锁情况下,虽然也需要while(true)循环一直检查,但cpu始终属于当前线程,线程就可以一直高速运行下去。但这也是保证此线程能分到cpu时间片的情况下,所以要保证核心数的充足,即进行cas操作的线程数不要超过核心数。
Java提供的一个cas的封装Atomic类,提供了一些原子操作
比如AtomicInteger的自增操作,底层会调用unsafe类的cas操作,比如AtomicBoolean,AtomicInteger,AtomicLong
CAS的ABA问题
假设有两个线程,线程1读取到内存值A,线程1时间片用完,切换到线程2,线程2也读取到了内存值A,并把它修改为B值,然后再把B值还原到A值,简单说,修改次序是A->B->A,接着线程1恢复运行,它发现内存值还是A,然后执行C A S操作,这就是著名的ABA问题,但是好像又看不出什么问题。
再举一个生活中的例子:朋友钱包的钱,我偷偷拿走了100,晚上再趁朋友没注意放100到里面,算不算偷窃?肯定算是吧
ABA问题的解决思路就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加一,那么A-B-A 就会变成1A-2B-3A。
从Java1.5开始JDK的atomic包里提供了一个类AtomicStampedReference来解决ABA问题。这个类的compareAndSet方法作用是首先检查当前引用是否等于预期引用,并且当前版本号是否等于预期版本号,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。