CAS——高效的秘诀来源于保持乐观

163 阅读4分钟

什么是CAS机制

CAS 是英文单词 compareAndSet / compareAndSet 的缩写,翻译过来就是比较并替换(赋值)。

CAS机制当中使用了3个基本操作数:内存地址V,旧的预期值A,要修改的新值B。 更新一个变量的时候,只有当变量的预期值A和内存地址V当中的实际值相同时,才会将内存地址V对应的值修改为B。

CAS 的特点

结合 CAS 和 volatile 可以实现无锁并发,适用于线程数少、多核 CPU 的场景下。

CAS 是基于乐观锁的思想:最乐观的估计,不怕别的线程来修改共享变量,就算改了也没关系,我吃亏点再重试呗。

synchronized 是基于悲观锁的思想:最悲观的估计,得防着其它线程来修改共享变量,我上了锁你们都别想改,我改完后解开锁,你们才有机会。

CAS 体现的是无锁并发、无阻塞并发,请仔细体会这两句话的意思

  • 因为没有使用 synchronized,所以线程不会陷入阻塞,这是效率提升的因素之一

  • 但如果竞争激烈,可以想到重试必然频繁发生,反而效率会受影响

为什么无锁效率高?

1、无锁情况下,即使重试失败,线程始终在高速运行,没有停歇,而 synchronized 会让线程在没有获得锁的时候,发生上下文切换,进入阻塞。

2、 线程就好像高速跑道上的赛车,高速运行时,速度超快,一旦发生上下文切换,就好比赛车要减速、熄火,等被唤醒又得重新打火、启动、加速... 恢复到高速运行,代价比较大

3、但无锁情况下,因为线程要保持运行,需要额外 CPU 的支持,CPU 在这里就好比高速跑道,没有额外的跑道,线程想高速运行也无从谈起,虽然不会进入阻塞,但由于没有分到时间片,仍然会进入可运行状态,还是会导致上下文切换。

原子类

J.U.C 并发包提供了:

  • AtomicBoolean
  • AtomicInteger
  • AtomicLong

原子整数

private AtomicInteger balance;
int prev = balance.get();
int next = prev - amount;
balance.compareAndSet(prev, next);

原子引用

  • AtomicReference
  • AtomicMarkableReference
  • AtomicStampedReference
AtomicReference<BigDecimal> ref;
public void withdraw(BigDecimal amount) {
    while (true) {
        BigDecimal prev = ref.get();
        BigDecimal next = prev.subtract(amount);
        if (ref.compareAndSet(prev, next)) {
            break;
        }
    }
}

AtomicStampedReference 关心引用变量变更的次数

static AtomicStampedReference<String> ref = new AtomicStampedReference<>("A", 0);
// 获取值 A
String prev = ref.getReference();
// 获取版本号
int stamp = ref.getStamp();
//CAS过程中增加一个版本号比较
ref.compareAndSet(prev, "C", stamp, stamp + 1)

AtomicMarkableReference 只关心引用变量是否变更过

GarbageBag bag = new GarbageBag("装满了垃圾");
// 参数2 mark 可以看作一个标记,表示垃圾袋满了
AtomicMarkableReference<GarbageBag> ref = new AtomicMarkableReference<>(bag, true);
GarbageBag prev = ref.getReference();
ref.compareAndSet(prev, new GarbageBag("空垃圾袋"), true, false);

原子数组

  • AtomicIntegerArray
  • AtomicLongArray
  • AtomicReferenceArray
demo(
 ()-> new AtomicIntegerArray(10),
 (array) -> array.length(),
 (array, index) -> array.getAndIncrement(index),
 array -> System.out.println(array)
);

字段更新器

  • AtomicReferenceFieldUpdater // 域 字段
  • AtomicIntegerFieldUpdater
  • AtomicLongFieldUpdater

利用字段更新器,可以针对对象的某个域(Field)进行原子操作,只能配合 volatile 修饰的字段使用,否则会出现异常

原子累加器

new LongAdder().increment()

new AtomicLong().getAndIncrement()

LongAdder较AtomicLong性能有所提升,在有竞争时,设置多个累加单元,Therad-0 累加Cell[0],而Thread-1 累加Cell[1],最后将结果汇总。因此它们在累加时操作的不同的Cell变量,因此减少了 CAS 重试失败,从而提高性能。

Unsafe

Unsafe 对象提供了非常底层的,操作内存、线程的方法,Unsafe 对象不能直接调用,只能通过反射获得

public class UnsafeAccessor {
    static Unsafe unsafe;
    static {
        try {
            Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
            theUnsafe.setAccessible(true);
            unsafe = (Unsafe) theUnsafe.get(null);
        } catch (NoSuchFieldException | IllegalAccessException e) {
            throw new Error(e);
        }
    }
    static Unsafe getUnsafe() {
        return unsafe;
    }
}

使用 cas 方法替换成员变量的值

UnsafeAccessor.unsafe.compareAndSwapInt(student, idOffset, 0, 20); // 返回 true

UnsafeAccessor.unsafe.compareAndSwapObject(student, nameOffset, null, "张三"); // 返回 true

CAS的缺点

1.CPU开销较大

在并发量比较高的情况下,如果许多线程反复尝试更新某一个变量,却又一直更新不成功,原地自旋,会给CPU带来很大的压力。

2.不能保证代码块的原子性

CAS机制所保证的只是一个变量的原子性操作,而不能保证整个代码块的原子性。比如需要保证3个变量共同进行原子性的更新,就不得不使用Synchronized了。

3.ABA问题

ABA问题是指如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了。可在变量前面追加上版本号,每次变量更新的时候把版本号加一,那么A-B-A 就会变成1A-2B-3A

AtomicStampedReference可解决ABA问题。这个类的compareAndSet方法作用是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。