CAS是一种无锁设计,通过乐观锁的思想来保证操作的原子性。
CAS是compare and swap的缩写(或者compare and set),它包含三个操作数——内存位置、预期原值及更新值。执行CAS操作的时候,将内存位置的值与预期原值比较:如果相匹配,那么处理器会自动将该位置值更新为新值,如果不匹配,处理器不做任何操作,多个线程同时执行CAS操作只有一个会成功。
CAS原理
有两个线程同时加用户积分,取出的时候是10,每个线程加5积分,预期结果积分应该是20,由于取出和设置不是原子操作,导致最终结果是15.使用了CAS结果也是15,不过会提示另外一个请求加失败了。
AtomicInteger atomicInteger = new AtomicInteger(10);
//取出旧的积分
int oldVal = atomicInteger.get();
//模拟线程一增加积分,期望值10,真实值10,返回true,加成功了
log.info(atomicInteger.compareAndSet(oldVal, oldVal + 5) + "");
//模拟线程二增加积分,期望值10,真实值15,返回false,加失败了
log.info(atomicInteger.compareAndSet(oldVal, oldVal + 5) + "");
流程图如下
从上图可以看出cas操作分为比较和设置,如何保证这个操作的原子性呢?
CAS的原子性是由CPU的原子指令(cmpxchg指令)来保证的,执行cmpxchg指令的时候,会判断当前系统是否为多核系统,如果是就给总线加锁,只有一个线程会对总线加锁成功,加锁成功之后会执行cas操作, 其实在这一点上还是有排他锁的,只是比起用synchronized, 这里的排他时间要短的多, 所以在多线程情况下性能会比较好。
自旋
上面的例子中如果数值有被改动就会返回失败,如何保证一定能设置成功呢?这是就需要用到自旋了。
AtomicInteger atomicInteger = new AtomicInteger(10);
int expect;
do {
expect = atomicInteger.get();
//不断重试直到设置成功
} while (!atomicInteger.compareAndSet(expect, expect + 5));
为什么CAS效率高
无锁情况下,即使重试失败,线程始终在高速运行,不会发生线程切换,而synchronized会让线程阻塞,发生上下文切换。
那CAS一定就效率高么?那也不一定,如果值被频繁改动,cas需要不断重试,这时效率就低了。
那CAS不会发生线程的切换么?那也不一定,如果CPU核心数不够,线程很多,发生激烈的争抢,同样会导致线程的切换,效率也高不起来。
原子整数
基于CAS,JUC工具包提供了一整套的原子操作类来保证常见操作的原子性。
首先我们来看整数的原子操作
AtomicInteger i = new AtomicInteger(0);
//实现 i++,先返回再加加
int result = i.getAndIncrement();
//实现++i,先加加再返回
result = i.incrementAndGet();
//实现i = i + 5
result = i.addAndGet(5);
//实现复杂操作逻辑,结果*10再返回
result = i.updateAndGet(value -> value * 10);
原子引用
首先要明确一个误区就是原子引用无法保证他修饰对象方法的原子性,也无法判断修饰对象的内部的内容变更了,他只能保证多个线程对引用修改时的线程安全性。
AtomicReference<BigDecimal> atomicReference = new AtomicReference<>(new BigDecimal(10));
BigDecimal money;
do {
money = atomicReference.get();
//比较的是引用
} while (!atomicReference.compareAndSet(money, money.subtract(new BigDecimal(5))));
ABA问题
原子引用有个ABA问题就是一个线程获取到引用A,这时另外一个线程把引用改成B,再改回到A,第一个线程是无法感知的,大多数情况下这个不是问题,对第一个线程来说,他只要在A的基础上操作就没问题,但有些情况下,需要对这种情况能够感知,这时就需要通过版本号来控制了,每次改动版本号加1,只有引用和版本号都对的情况下才认为没有改动。
//初始值为A,版本号为0
AtomicStampedReference<String> atomicReference = new AtomicStampedReference<>("A", 0);
while (true) {
String prev = atomicReference.getReference();
int stamp = atomicReference.getStamp();
if (atomicReference.compareAndSet(prev, prev + "B", stamp, stamp + 1)) {
//设置成功,退出循环
break;
}
}
原子数组
原子数组可以保证数组元素操作的原子性
//原子数组
AtomicIntegerArray atomicIntegerArray = new AtomicIntegerArray(10);
//第1个元素自增
atomicIntegerArray.getAndIncrement(0);
//第二个元素加2
atomicIntegerArray.getAndAdd(1, 2);
//第三个元素乘以10
atomicIntegerArray.updateAndGet(2, value -> value * 10);