likes
comments
collection
share

深挖原理之CAS锁

作者站长头像
站长
· 阅读数 10

高并发是面试中常被问到的问题,而高并发中归根究底就是个资源共享线程安全问题,是常用解决方案。 CAS乐观锁又常被提及,我们这篇文章简单说下CAS锁,并向下挖挖,看看其底层原理。

什么是CAS锁呢?

直译就是Compare and Set,比较和设值。这里会有两种情况

  • 先比较值与期待的是否一致,若是一致,则赋值,并返回true
  • 先比较值与期待的是否一致,不一致,则不赋值,并返回false

CAS的底层实现

Unsafe类里有CAS的对应方法,这里举三个例子。

import sun.misc.Unsafe;
@HotSpotIntrinsicCandidate
public final native boolean compareAndSetInt(Object o, long offset,int expected,int x);
@HotSpotIntrinsicCandidate
public final native boolean compareAndSetObject(Object o, long offset,Object expected,Object x);
@HotSpotIntrinsicCandidate
public final native boolean compareAndSetLong(Object o, long offset,long expected,long x);

这已经是native方法了,在java里我们最多也就能看到这里了,它会直接调用本地依赖库中的C++方法,既然如此,我们就去C++里瞅瞅。 直接给到位置,在这个unsafe.cpp中,有我们需要的东西。

https://hg.openjdk.org/jdk10/jdk10/hotspot/file/5ab7a67bc155/src/share/vm/prims/unsafe.cpp
UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSetInt(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jint e, jint x)) {
  oop p = JNIHandles::resolve(obj);
  jint* addr = (jint *)index_oop_from_field_offset_long(p, offset);

  return (jint)(Atomic::cmpxchg(x, addr, e)) == e;
} UNSAFE_END

上边这段代码中最最最主要的信息就是return (jint)(Atomic::cmpxchg(x, addr, e)) == e;这里的这个Atomic::cmpxchg()方法。cmpxchg也就是compare and exchange,最终走的是这个位置。那么这个方法又指向哪里了呢?

继续深入,我们能找到这样一个方法:

inline jlong    Atomic::cmpxchg    (jlong    exchange_value, volatile jlong*    dest, jlong    compare_value) {
  //判断CPU是否是多核
  bool mp = os::is_MP();
  __asm__ __volatile__ (LOCK_IF_MP(%4) "cmpxchgq %1,(%3)"
                        : "=a" (exchange_value)
                        : "r" (exchange_value), "a" (compare_value), "r" (dest), "r" (mp)
                        : "cc", "memory");
  return exchange_value;
}

在这里先判断了操作系统是否是多核的,倘若是多核的,那么会添加一个lock指令。 所以对于单核操作系统来说,cmpxchgq这个指令是具有原子性的,它不能再被拆分。 但是对于多核的情况,cmpxchgq并不能保证原子性,在多个线程执行cmpxchgq的操作同一个数据的时候若是没有锁,则依旧可能出现问题。 __asm__ 这个东西则是指后边的指令是汇编的指令,而汇编指令是直接操作的硬件。 那么cmpxchgq这个指令就是汇编指令,代表着cpu本就支持比较和交换这个命令。 再说一下,lock锁会有缓存行级别的锁和总线锁,cpu会根据实际情况自行选择。

CAS的常见问题

ABA问题

先解释一下ABA问题

  1. 线程1期待将value值从A改到B,
  2. 线程2期待将value值从B改到A,
  3. 线程3期待将value值从A改到C

假设value初始值为A这样当三个线程同时执行,从主内存取值的时候,线程1和3都取到了自己期待的值, 这时候,线程1先执行的,将A改为了B, 然后线程2执行,将B改回了A, 然后线程三去取值比较发现就是自己期待的A,然后取值,将A改为C。但是这时候的A已经不是之前的A了,但它也能执行成功。 这里虽然三个线程都执行CAS成功了,但是线程3有ABA的问题

简单说,就是我取值的时候,取到的是期待的值,然后做处理,处理完去取值比较的时候,取到的还是期待的值,但是这次取到的值虽然和之前的值一样,但是这已经不是之前的值了。

其实这不一定是个问题,在操作数字的++--的时候,它所做的就是加一减一,并不要求目前的值是否是自己期待的原本的值。只要是它期待的数字,去++--就行。

解决方案

在执行比较的时候增加一个版本号,修改一次则加一,比较的时候,同时比较值和版本号。

这个在JUC的AtomicStampedReference里有具体的实现,在执行cas的时候,不仅传入了值,并且传入了两个版本号,比较的时候同时会比较版本号是否相同。

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)));
}

自旋次数过多的问题

这里的自旋是指,CAS锁,一直在取值比较,发现不是自己期待的值,又重新取值比较,一直在取,在比较,这会占用过多的CPU资源。 在Unsafe类中的getAndAddInt方法我们可以看到,它会一直循环,直到cas成功。若是线程被挂起,那么cpu会一直调度这个线程,直到成功。

@HotSpotIntrinsicCandidate
public final int getAndAddInt(Object o, long offset, int delta) {
    int v;
    do {
        v = getIntVolatile(o, offset);
    } while (!weakCompareAndSetInt(o, offset, v, v + delta));
    return v;
}

解决方案

有两个思路方向。

  • synchronized方向:从CAS几次失败后,就将线程挂起(WAITING),避免占用CPU过多的资源!
  • LongAdder方向:这里是基于类似 分段锁 的形式去解决(要看业务,有限制的),传统的AtmoicLong是针对内存中唯一的一个值去++,LongAdder在内存中搞了好多个值,多个线程去加不同的值,当你需要结果时,会将所有值累加,返回给你。