CAS自旋

 

1、CAS是什么?

比较并交换(compare and swap, CAS),是原子操作的一种。在多线程没有锁的状态下,可以保证多个线程对同一个值的更新

通常指的是这样一种原子操作:针对一个变量,首先比较它的内存值与某个期望值是否相同,如果相同,就给它赋一个新值。

CAS的伪代码的逻辑

if (value == expectedValue) {
    value = newValue;
}
CAS 可以看作是它们合并后的整体一个不可分割的原子操作,并且其原子性是直接在硬件层面得到保障的。

在多线程场景下,不加锁的情况下来修改值,CAS是怎么自旋的呢

 以上图举例说明:

1、现在Data中存放的是N=0,线程A将N=0拷贝到自己的工作内存中,即E等于N的值,E=0,做加1操作,V=E+1=1

2、由于是在多线程不加锁的场景下操作,所以可能此时N被别的线程修改为其他值,此时需要再次读取N看其是否被修改

3、如果被修改,即E != N,说明被其他线程修改过,那么此时工作内存中的E已经和主存中的N不一致了,需要重新读取N的值拷贝到工作内存E中,直到E = N才能修改

4、如果没被修改,即E= N ,说明没被其他线程修改过,那么将工作内存中的E=0改为E=1,同时写回主存,将N=0改为N=1。

 

2、ABA问题

1、什么是ABA问题?

在线程A计算V的时候,可能线程B将N=0改为了N=2,线程C又将N=2改为了N=0,此时的N值虽然还是0,还是N已经不是一开始的N了

2、如何解决ABA问题?

1、添加版本号,我们把值N加一个版本号tag,当有线程修改的时候,版本号就会发生变化。在读取E的时候,同时将版本号tag也读取上,在比较E == N的时候,同时比较tag是否发生了改变

实际应用此方案的有:AtomicStampedReference类

2、添加时间戳也可以解决。查询的时候把时间戳一起查出来,对的上才修改并且更新值的时候一起修改更新时间,这样也能保证,方法很多但是跟添加版本号都是异曲同工之妙。

 

3、基于CAS的应用

基于CAS的应用有 信号量Semaphore、juc原子类

juc原子类:

基本类型 AtomicInteger、AtomicLong、AtomicBoolean
引用类型 AtomicReference、AtomicStampedRerence、AtomicMarkableReference
数组类型 AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray
对象属性原子修改器 AtomicIntegerFieldUpdater、AtomicLongFieldUpdater、AtomicReferenceFieldUpdater
原子类型累加器(jdk1.8增加的类) DoubleAccumulator、DoubleAdder、LongAccumulator、LongAdder、Striped64

 

4、CAS源码分析

CAS的应用:Unsafe类
public native boolean compareAndSwapInt(Object obj, long offset, int expect, int update);
public native boolean compareAndSwapLong(Object obj, long offset, long expect, long update);
public native boolean compareAndSwapObject(Object obj, long offset, Object expect, Object update)

可以看到是native修饰的方法,native说明是通过jni调用C++的代码了,我们就来看一下C++中是怎么实现的CAS。下面是unsafe.cpp中代码实现

复制代码
#unsafe.cpp
UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapInt(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jint e, jint x))
  UnsafeWrapper("Unsafe_CompareAndSwapInt");
  oop p = JNIHandles::resolve(obj);
  // 根据偏移量,计算value的地址
  jint* addr = (jint *) index_oop_from_field_offset_long(p, offset);
  // Atomic::cmpxchg(x, addr, e) cas逻辑 x:要交换的值   e:要比较的值
  //cas成功,返回期望值e,等于e,此方法返回true 
  //cas失败,返回内存中的value值,不等于e,此方法返回false
  return (jint)(Atomic::cmpxchg(x, addr, e)) == e;
复制代码

核心逻辑在Atomic::cmpxchg方法中,这个根据不同操作系统和不同CPU会有不同的实现。这里我们以linux_64x的为例,查看Atomic::cmpxchg的实现

复制代码
#atomic_linux_x86.inline.hpp
inline jint     Atomic::cmpxchg    (jint     exchange_value, volatile jint*     dest, jint     compare_value) {
  //判断当前执行环境是否为多处理器环境
  int mp = os::is_MP();
  //LOCK_IF_MP(%4) 在多处理器环境下,为 cmpxchgl 指令添加 lock 前缀,以达到内存屏障的效果
  //cmpxchgl 指令是包含在 x86 架构及 IA-64 架构中的一个原子条件指令,
  //它会首先比较 dest 指针指向的内存值是否和 compare_value 的值相等,
  //如果相等,则双向交换 dest 与 exchange_value,否则就单方面地将 dest 指向的内存值交给exchange_value。
  //这条指令完成了整个 CAS 操作,因此它也被称为 CAS 指令。
  __asm__ volatile (LOCK_IF_MP(%4) "cmpxchgl %1,(%3)"
                    : "=a" (exchange_value)
                    : "r" (exchange_value), "a" (compare_value), "r" (dest), "r" (mp)
                    : "cc", "memory");
  return exchange_value;
复制代码

cmpxchgl的详细执行过程:

  输入参数是"r" (exchange_value), “a” (compare_value), “r” (dest), “r” (mp),参数前面的r表示通用寄存器,a表示eax寄存器,即 compare_value存入 eax寄存器,而 exchange_value、dest、mp的值存入任意的通用寄存器。嵌入式汇编规定把输出和输入寄存器按统一顺序编号,顺序是从输出寄存器序列从左到右从上到下以”0%”开始,分别记为%0 ,%1....%9。也就是说,输出的 eax是%0,输入的 exchange_value、compare_value、dest、mp分别是%1,%2,%3,%4。

  因此 cmpxchgl %1,(%3) 实际上表示的是 cmpxchgl exchange_value,(dest),需要注意的是 cmpxchgl 有个隐含操作数,其实际过程是先比较eax (即 compare_value )的值和 dest地址所存的值是否相等。输出参数 "=a" (exchange_value) 表示把eax中存的值写入 eachange_value 变量中。

  执行cmpxchgl指令时,如果 compare_value 和 dest 指针指向内存的值相等,则会使得 dest 指针指向内存的值变成 exchange_value,最终 eax 的 compare_value赋值给 exchange_value 变量,即函数最终返回值是原先的 compare_value ,此时 Unsafe_CompareAndSwapInt 函数的返回值 (jint)(Atomic::cmpxchg(x, addr, e)) == e 就是true,表示CAS成功

  执行cmpxchgl指令时,如果 compare_value 和 dest 不相等,则会将当前 dest 指针指向内存的值写入 eax,最终输出时赋值给 exchange_value 变量作为返回值,导致 (jint)(Atomic::cmpxchg(x, addr, e)) == e 结果为false,表示CAS失败

 

LOCK_IF_MP(%4)方法内部为

#define LOCK_IF_MP(mp) "cmp $0, " #mp "; je 1f; lock; 1: "

LOCK_IF_MP(mp)函数中的入参mp全称是Multi Processor ,多CPU。

注意这个函数里有个lock; 1,意思是什么呢,就是看你操作系统有多少个处理器,如果只有一个cpu一核的话就不需要原子性了,一定是顺序执行的,如果是多核心多cpu前面就要加lock。

所以最终实现CAS的汇编指令是lock cmpxchgl 指令

 

posted @   harara  阅读(297)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· 三行代码完成国际化适配,妙~啊~
· .NET Core 中如何实现缓存的预热?
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
历史上的今天:
2021-11-11 Java 8 Stream 使用
2021-11-11 Netty实现Http客户端【支持https请求】
点击右上角即可分享
微信分享提示

目录导航