CAS是什么
CAS是compare and swap的缩写,即我们所说的比较交换。该操作的作用就是保证数据一致性、操作原子性。
cas是一种基于锁的操作,而且是乐观锁
。在java中锁分为乐观锁和悲观锁。悲观锁是将资源锁住,等之前获得锁的线程释放锁之后,下一个线程才可以访问。而乐观锁
采取了一种宽泛的态度,通过某种方式不加锁来处理资源,比如通过给记录加version
来获取数据,性能较悲观锁有很大的提高。
CAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。如果内存地址里面的值和A的值是一样的,那么就将内存里面的值更新成B。否则将不做任何处理。
CAS是通过无限循环来获取数据的,若果在第一轮循环中,a线程获取地址里面的值被b线程修改了,那么a线程需要自旋,到下次循环才有可能机会执行。
为什么说CAS能很好的保证数据一致性,因为它是直接从硬件层面保证了原子性。
CAS是一条CPU的原子指令(cmpxchg指令),Unsafe提供的CAS方法(如compareAndSwapXXX)底层实现即为CPU指令cmpxchg。
执行cmpxchg指令的时候,会判断当前系统是否为多核系统,如果是就给总线加锁,只有一个线程会对总线加锁成功,加锁成功之后会执行cas操作,也就是说CAS的原子性实际上是CPU实现独占的。比起用synchronized重量级锁, 这里的排他时间要短很多, 所以在多线程情况下性能会比较好。
测试案例讲解
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicInteger;
public class TestCas {
public static int count = 0;
private final static int MAX_TREAD=10;
public static AtomicInteger atomicInteger = new AtomicInteger(0);
public static void main(String[] args) throws InterruptedException {
/*CountDownLatch能够使一个线程在等待另外一些线程完成各自工作之后,再继续执行。
使用一个计数器进行实现。计数器初始值为线程的数量。当每一个线程完成自己任务后,计数器的值就会减一。
当计数器的值为0时,表示所有的线程都已经完成一些任务,然后在CountDownLatch上等待的线程就可以恢复执行接下来的任务。*/
CountDownLatch latch = new CountDownLatch(MAX_TREAD);
//匿名内部类
Runnable runnable = new Runnable() {
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
count++;
atomicInteger.getAndIncrement();
}
latch.countDown(); // 当前线程调用此方法,则计数减一
}
};
//同时启动多个线程
for (int i = 0; i < MAX_TREAD; i++) {
new Thread(runnable).start();
}
latch.await(); // 阻塞当前线程,直到计数器的值为0
System.out.println("理论结果:" + 1000 * MAX_TREAD);
System.out.println("static count: " + count);
System.out.println("AtomicInteger: " + atomicInteger.intValue());
}
}
我们发现每次运行,atomicInteger 的结果值都是正确的,count++的结果却不对
为什么AtomicInteger
类的操作能保证数据一致性呢个?进入它的源码:
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
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;
}
//cas方法
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
可以看到最终是调用了Unsafe类中的compareAndSwapInt
,该方法就是CAS方法其中的一个。Unsafe类中总共有三个涉及CAS的方法
/*
@param o 包含要修改的字段的对象
@param offset 字段在对象内的偏移量
@param expected 期望值(旧的值)
@param update 更新值(新的值)
@return true 更新成功 | false 更新失败
*/
public final native boolean compareAndSwapObject(Object o, long offset, Object expected, Object update);
public final native boolean compareAndSwapInt( Object o, long offset, int expected,int update);
public final native boolean compareAndSwapLong( Object o, long offset, long expected, long update);
在执行 Unsafe 的 CAS 方法的时候,这些方法首先将内存位置的值与预期值(旧的值)比较,如果相匹配,那么处理器会自动将该内存位置的值更新为新值,并返回 true ;如果不相匹配,处理器不做任何操作,并返回 false 。
总的来说,AtomicInteger
类主要利用CAS (compare and swap) + volatile和 native
方法来保证原子操作。
通过看上述getAndAddInt
方法源码,可见如果cas失败会不断循环重复尝试。这就常说的cas自旋
,所谓自旋其实是重复调用compareAndSwap
方法,涉及到的方法有以下几个,也是在Unsafe
类中。
- getAndAddInt
- getAndAddLong
- getAndSetInt
- getAndSetLong
- getAndSetObject
CAS缺点
-
ABA问题。因为CAS需要在操作值的时候检查下值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了。
-
循环时间长开销大。高并发下N多线程同时去操作一个变量,会造成大量线程CAS失败,然后处于自旋状态,导致严重浪费CPU资源,降低了并发性。
ABA问题解决方案(AtomicStampedReference)
JDK 的提供了一个类似 AtomicStampedReference
类来解决 ABA 问题。
AtomicStampReference 在 CAS 的基础上增加了一个 Stamp 整型 印戳(或标记),使用这个印戳可以来觉察数据是否发生变化,给数据带上了一种实效性的检验。
AtomicStampReference 的 compareAndSet
方法首先检查当前的对象引用值是否等于预期引用,并且当前印戳( Stamp )标志是否等于预期标志,如果全部相等,则以原子方式将引用值和印戳( Stamp )标志的值更新为给定的更新值。
-
AtomicStampReference 的构造器
/** * @param initialRef初始引用 * @param initialStamp初始戳记 */ AtomicStampedReference(V initialRef, int initialStamp)
-
AtomicStampReference的核心方法
AtomicStampReference
类中有自己的compareAndSet
方法,进入源码:/** * expectedReference 引用的旧值 * newReference 引用的新值 * expectedStamp 旧的戳记 * newStamp 新的戳记 */ public boolean compareAndSet(V expectedReference, V newReference, int expectedStamp, int newStamp) { Pair<V> current = pair; return expectedReference == current.reference && expectedStamp == current.stamp && ((newReference == current.reference && newStamp == current.stamp) || casPair(current, Pair.of(newReference, newStamp))); //unsafe类中的cas }
可以看到旧值
expectedReference
和旧印戳expectedStamp
都会进行比较,都满足才会调用Unsafe中的cas方法。