深挖原理之CAS锁
高并发是面试中常被问到的问题,而高并发中归根究底就是个资源共享,线程安全问题,锁是常用解决方案。 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期待将value值从A改到B,
- 线程2期待将value值从B改到A,
- 线程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在内存中搞了好多个值,多个线程去加不同的值,当你需要结果时,会将所有值累加,返回给你。
转载自:https://juejin.cn/post/7220375870228332602