服务粉丝

我们一直在努力
当前位置:首页 > 财经 >

似懂非懂 CAS ?这次全都能搞明白

日期: 来源:郭霖收集编辑:小虾米君


/   今日科技快讯   /

近日,在2023亚布力中国企业家论坛年会现场,百度创始人、董事长兼首席执行官李彦宏在接受采访时,首次回应外界对于文心一言的反馈。“外界反馈跟我预期差不多。”李彦宏表示,“你去看ChatGPT在刚推出的时候,外界反馈比文心一言还要糟糕。”他表示,一直在关注文心一言内测后的反馈。

/   作者简介   /

大家周一好,新的一周继续加油!

本篇文章转自TechMerger的博客,文章主要分享了对CAS机制的相关探索,相信会对大家有所帮助!

原文地址:
https://juejin.cn/post/7209578803159924796

/   前言   /

求学、面试的时候会无法回避 CAS 话题,但对于其原理,总有种似懂非懂的感觉。

CAS 机制全称:Compare and Swap,即比较并替换 。也有叫做 Compare and Set 的,即比较并设置。顾名思义,分为两步:

  1. 比较:读取到了一个值 A,在将其更新为 B 之前,检查原值是否仍为 A
  2. 替换 / 设置:YES 则将 A 更新为 B,结束;反之,重复上述操作直到成功为止 

这种机制在确保原子化操作、实现乐观锁的同时也无法避免一些缺陷,咱们从源码入手分析一下其原理、乐观锁和缺陷等各个细节。

/   源码解读   /

以 JDK 中最常用的 AtomicInteger 类的源码为例进行探讨,版本为 JDK 8。 

Unsafe

首先 AtomicInteger 将通过 Unsafe 提供的静态方法 getUnsafe() 获得其单例。

 // java.util.concurrent.atomic.AtomicInteger
 public class AtomicInteger extends Number implements java.io.Serializable {
     private static final Unsafe unsafe = Unsafe.getUnsafe();
     ...
 }
 
 // jdk.internal.misc.Unsafe
 public final class Unsafe {
     ...
     private Unsafe() {}
 
     private static final Unsafe theUnsafe = new Unsafe();
 
     @CallerSensitive
     public static Unsafe getUnsafe() {
         ...
         return theUnsafe;
     }
 }

为什么叫 Unsafe 呢?

因为它提供了针对任意地址的数据进行不安全读写等内存操作、线程调度、CAS 等操作的入口,如果调用是不受信任的,那么使得代码出错的概率变大,也更会引发 JVM 级别的 exception。所以要求只有可信任的代码可以获取其单例并进行调用,所以将其命名为 Unsafe,给调用者以提醒。正因为此,getUnsafe() 内部在返回 Unsafe 单例前会先去检查调用 Class 其是否可信任,如果不是系统 ClassLoader 加载的 Class 的话,会抛出 SecurityException。

 // jdk.internal.misc.Unsafe
 public final class Unsafe {
     ...
     @CallerSensitive
     public static Unsafe getUnsafe() {
         Class<?> caller = Reflection.getCallerClass();
         if (!VM.isSystemDomainLoader(caller.getClassLoader()))
             throw new SecurityException("Unsafe");
         return theUnsafe;
     }
 }
 
 // sun.misc.VM
 public class VM {
     ...
     public static boolean isSystemDomainLoader(ClassLoader loader) {
         return loader == null;
     }
 }

并建议像如下示例代码一样进行使用:

  class MyTrustedClass {
     private static final Unsafe unsafe = Unsafe.getUnsafe();
     ...
     private long myCountAddress = ...;
     public int getCount() { return unsafe.getByte(myCountAddress); }
 }

valueOffset & value

其次,赋值内部持有相关变量,包括目标的 int 型数值 value 和关联该 value 地址的 long 型的 valueOffset:

  • value 使用 volatile 修饰,默认为 0,反之为 AtomicInteger 构造时指定的初始值 initialValue
  • valueOffset 是存放 value 的内存相对地址:在 static 块中通过 Unsafe 的 objectFieldOffset() 传入的 value 的 Field 字段得到。

 // java.util.concurrent.atomic.AtomicInteger
 public class AtomicInteger extends Number implements java.io.Serializable {
     ...
     private volatile int value;
 
     public AtomicInteger(int initialValue) {
         value = initialValue;
     }
 
     public AtomicInteger() { }
 
     private static final long valueOffset;
 
     static {
         try {
             valueOffset = unsafe.objectFieldOffset
                 (AtomicInteger.class.getDeclaredField("value"));
         } catch (Exception ex) { throw new Error(ex); }
     }
 }
 
 // jdk.internal.misc.Unsafe
 public final class Unsafe {
     ...
     public native long objectFieldOffset(Field f);
 }

Unsafe_ObjectFieldOffset 的实现是调用 find_field_offset 进行,其将进行 Field 的非空检查,以及 Klass 的转化。最后还要转换成 InstanceKlass 实例,并获取其中的 field_offset 并返回。

  // unsafe.cpp
 UNSAFE_ENTRY(jlong, Unsafe_ObjectFieldOffset(JNIEnv *env, jobject unsafe, jobject field))
   UnsafeWrapper("Unsafe_ObjectFieldOffset");
   return find_field_offset(field, 0, THREAD);
 UNSAFE_END

 jint find_field_offset(jobject field, int must_be_static, TRAPS) {
   if (field == NULL) {
     THROW_0(vmSymbols::java_lang_NullPointerException());
   }
 
   oop reflected   = JNIHandles::resolve_non_null(field);
   oop mirror      = java_lang_reflect_Field::clazz(reflected);
   Klass* k      = java_lang_Class::as_Klass(mirror);
   int slot        = java_lang_reflect_Field::slot(reflected);
   int modifiers   = java_lang_reflect_Field::modifiers(reflected);
 
   if (must_be_static >= 0) {
     int really_is_static = ((modifiers & JVM_ACC_STATIC) != 0);
     if (must_be_static != really_is_static) {
       THROW_0(vmSymbols::java_lang_IllegalArgumentException());
     }
   }
 
   int offset = InstanceKlass::cast(k)->field_offset(slot);
   return field_offset_from_byte_offset(offset);
 }
 
 // instanceKlass.h
 class InstanceKlass: public Klass {
   ...
  public:
   int     field_offset      (int index) const { return field(index)->offset(); }
 
     // Casting from Klass*
   static InstanceKlass* cast(Klass* k) {
     assert(k == NULL || k->is_klass(), "must be");
     assert(k == NULL || k->oop_is_instance(), "cast to InstanceKlass");
     return (InstanceKlass*) k;
   }
 }

compareAndSwap

后面是在写值的时候继续调用 Unsafe,关键在于其 native 侧的实现。

比如 compareAndSet() 将传递实例自身,value 的内存相对地址,当前的期待值和目标的更新值四者传递给 Unsafe:

  • 返回 false 表示当前的值并非期待值,无法完成更新
  • 返回 true 表示更新成功

 // java.util.concurrent.atomic.AtomicInteger
 public class AtomicInteger extends Number implements java.io.Serializable {
     private static final long valueOffset;
     ...
 
     public final boolean compareAndSet(int expect, int update) {
         return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
     }
 }
 
 // jdk.internal.misc.Unsafe
 public final class Unsafe {
     ...
     public final native boolean compareAndSwapInt(Object o, long offset,
                                                   int expected,
                                                   int x);
 }

native 的实现是 compareAndSwapInt()。

  • 先通过 JNIHandles 找到 AtomicInteger 实例的地址 oop
  • 将该 oop 和 value 的相对地址作为参数调用 index_oop_from_field_offset_long() 并得到 value 的内存绝对地址 aadr 
  • 将更新值、addr 和期待值作为参数调用 cmpxchg() 继续

 // 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);
   jint* addr = (jint *) index_oop_from_field_offset_long(p, offset);
   return (jint)(Atomic::cmpxchg(x, addr, e)) == e;
 UNSAFE_END

 inline void* index_oop_from_field_offset_long(oop p, jlong field_offset) {
   jlong byte_offset = field_offset_to_byte_offset(field_offset);
 #ifdef ASSERT
   if (p != NULL) {
     assert(byte_offset >= 0 && byte_offset <= (jlong)MAX_OBJECT_SIZE, "sane offset");
     if (byte_offset == (jint)byte_offset) {
       void* ptr_plus_disp = (address)p + byte_offset;
       assert((void*)p->obj_field_addr<oop>((jint)byte_offset) == ptr_plus_disp,
              "raw [ptr+disp] must be consistent with oop::field_base");
     }
     jlong p_size = HeapWordSize * (jlong)(p->size());
     assert(byte_offset < p_size, err_msg("Unsafe access: offset " INT64_FORMAT " > object's size " INT64_FORMAT, byte_offset, p_size));
   }
 #endif
   if (sizeof(char*) == sizeof(jint))    // (this constant folds!)
     return (address)p + (jint) byte_offset;
   else
     return (address)p +        byte_offset;
 }

cmpxchg() 的实现在 atomic.cpp 中,其中 value 的地址用 volatile 关键字描述,意味着目标数据的变化将立即反映到内存、并要求读取时应当从内存中取出。

  1. 首先计算得到 value 地址的当前数值,即 cur_as_bytes[offset]
  2. 假使当前数值等于期待值 compare_value:
    1. 调用 cmpxchg CPU 指令将 dest_int 地址对应的数值执行期待值为 cur,更新值为 new_val 的 CAS 操作
    2. 该操作返回的是期待值的话,表示更新成功,跳过循环并返回期待值。Unsafe_CompareAndSwapInt() 将收到匹配期待值的结果,并向 Java 侧返回 true
    3. 反之,将 cur 更新为 CPU 指令返回的当前值,再继续下一次循环,直到成功为止
  3. 反之即不等于期待值,直接返回
    1. Unsafe_CompareAndSwapInt() 将收到不匹配期待值的结果,并向 Java 侧返回 false

  // atomic.cpp
 jbyte Atomic::cmpxchg(jbyte exchange_value, volatile jbyte* dest, jbyte compare_value) {
   uintptr_t dest_addr = (uintptr_t)dest;
   uintptr_t offset = dest_addr % sizeof(jint);

   volatile jint* dest_int = (volatile jint*)(dest_addr - offset);
   jint cur = *dest_int;

   jbyte* cur_as_bytes = (jbyte*)(&cur);
   jint new_val = cur;

   jbyte* new_val_as_bytes = (jbyte*)(&new_val);
   new_val_as_bytes[offset] = exchange_value;

   while (cur_as_bytes[offset] == compare_value) {
     jint res = cmpxchg(new_val, dest_int, cur);
     if (res == cur) break;
     cur = res;
     new_val = cur;
     new_val_as_bytes[offset] = exchange_value;
   }
   return cur_as_bytes[offset];
 }

事实上,Android 中 AtomicInteger 的实现稍稍不同,没有只用 Unsafe 而是采用了 VarHandle,这里不再展开。

 // android/libcore/ojluni/src/main/java/java/
 // util/concurrent/atomic/AtomicInteger.java
 public class AtomicInteger extends Number implements java.io.Serializable {
     ...
     public final boolean compareAndSet(int expectedValue, int newValue) {
          // Android-changed: Using VarHandle instead of Unsafe
          // return U.compareAndSetInt(this, VALUE, expectedValue, newValue);
          return VALUE.compareAndSet(this, expectedValue, newValue);
      }
 }

/   乐观锁   /

利用 CAS 机制可以实现一个乐观锁,即无需加锁可实现多个线程同时读取、但是仅有一个线程可以成功写入数据,并导致其他要卸乳数据的线程回滚重试。

例如 Unsafe 通过上述的 compareAndSwapInt() 实现的自增 1 的原子操作的逻辑。

 // java.util.concurrent.atomic.AtomicInteger
 public class AtomicInteger extends Number implements java.io.Serializable {
     ...
     public final int getAndIncrement() {
         return unsafe.getAndAddInt(this, valueOffset, 1);
     }
 }
 
 // jdk.internal.misc.Unsafe
 public final class Unsafe {
     ...
     public final int getAndAddInt(Object o, long offset, int delta) {
         int v;
         do {
             v = getIntVolatile(o, offset);
         } while (!compareAndSwapInt(o, offset, v, v + delta));
         return v;
     }
 }

因为整个过程中不涉及加/解锁的操作,乐观锁也被称为无锁编程。

本质上说,乐观锁其实不是“锁”,它仅仅是一个循环重试 CAS 的算法而已!

/   缺陷   /

CAS 机制可以高效地实现原子操作,但仍不完美:

  1. 循环时间长开销大:CAS 大量失败后长时间占用 CPU 资源,加大了系统性能开销
  2. 只能保证一个共享变量的原子操作:当对一个共享变量执行操作时,我们可以使用循环 CAS 的方式来保证原子操作,但是对多个共享变量操作时,循环 CAS 就无法保证操作的原子性
  3. ABA 问题:CAS 机制本质上依赖值有没有发生变化的操作条件。但是如果值原来是 A、被改成变成了 B、最后又变回了 A,那么使用 CAS 进行检查时会发现它的值没有发生变化进行了操作,但是实际上却变化了,这其实违背了约定的条件。

Java 1.5 开始提供了一个类AtomicStampedReference 来解决该问题,其提供的 compareAndSet() 会首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,才进行值的更新操作。

/   结语   /

可以看到无论是初始化还是写值都是通过 Unsafe 调度完成,整理了如下的流程图以加深理解:


  1. AtomicInteger 通过静态方法 getUnsafe() 获得 Unsafe 实例
  2. 接着通过 Unsafe 实例的 native 方法 objectFieldOffset() 传入使用 volatie 修饰的数值 value 的 Field 字段得到其内存相对地址
  3. 关键的 compareAndSet() 亦由 Unsafe 调度,即 native 方法 compareAndSwapInt()。主要是传入 AtomicInteger 实例,value 内存相对地址,期待值以及更新值。
  4. native 的实现由 atomic.cpp 的 cmpxchg() 完成,当前 value 内存地址返回的值匹配期待值的话,执行更新操作;反之,直接返回 false。
  5. 执行更新的操作采用 cmpxchg CPU 指令,如果得到的值不符合期待值的话,更新期待值继续下一次循环,直到匹配为止。

推荐阅读:
我的新书,《第一行代码 第3版》已出版!
Android 14 Developer Preview一览
Android IM即时通信,设计一个聊天中间件吧

欢迎关注我的公众号
学习技术或投稿


长按上图,识别图中二维码即可关注

相关阅读

  • 系统调用和API有什么区别?

  • 大家好,我是小风哥。有很多同学在微信里问操作系统中的系统调用与API有什么区别,今天简单聊一聊这个话题。首先,什么是API呢?很简单,这就是API:这是发动机给你提供的api,当你想让汽
  • 边框宽度表现不错,魅族20 Pro正面渲染图曝光

  • 按照官方公布的信息,魅族20系列旗舰手机、新一代无界生态系统Flyme 10以及备受用户关注的 Flyme Auto,已经在进行最后的准备,将于3 月30日举行无界生态发布会。虽然之前魅族已
  • OPPO Find X6 系列、OPPO Pad 2参数再曝,3月21日发布

  • 不久前,OPPO官宣OPPO Pad 2将于3月21日14:00发布。目前,数码博主@数码闲聊站 曝光了 OPPO Pad 2的真机图,展示了该机的正面部分,并透露了此平板屏幕的部分参数信息。该博主表示,O
  • 售价28.58-37.23万元,捷尼赛思GV60正式上市

  • 随着现代起亚集团正式进入电气化转型,未来的一段时间将在全球市场投放多款电动车型。近日,捷尼赛思GV60正式上市,其中豪华版售价28.58万元起,旗舰版售价35.18万元起。新车定位纯
  • 一键去除所有限制,不光免费还吊打同行!

  • 前言今天介绍一款重磅软件,堪称最出色的视频保存工具。支持众多视频网站,下载的内容不仅仅是视频。另外两款小工具也是装机必备的神器。心动不如行动,我们不多说废话,直接往下看
  • 可扫码举报!河北1市最新公告

  • 秦皇岛市纪委监委最新发布关于受理破坏营商环境行为监督举报的公告↓↓↓秦皇岛市纪委监委关于受理破坏营商环境行为监督举报的公告为深入贯彻党的二十大精神,全面落实党中央
  • 高通第二代骁龙7+正式发布,Redmi、realme本月搭载

  • 今天,高通带来了旗下骁龙新品发布,正式推出了第二代骁龙7+移动平台。官方介绍显示,第二代骁龙7+采用Kyro CPU,相较前代产品,CPU性能提升高达50%;Adreno GPU性能提升2倍,在整个7系芯

热门文章

  • “复活”半年后 京东拍拍二手杀入公益事业

  • 京东拍拍二手“复活”半年后,杀入公益事业,试图让企业捐的赠品、家庭闲置品变成实实在在的“爱心”。 把“闲置品”变爱心 6月12日,“益心一益·守护梦想每一步”2018年四

最新文章

  • 似懂非懂 CAS ?这次全都能搞明白

  • / 今日科技快讯 /近日,在2023亚布力中国企业家论坛年会现场,百度创始人、董事长兼首席执行官李彦宏在接受采访时,首次回应外界对于文心一言的反馈。“外界反馈跟我预期差不
  • 真的没谁了,你想要的这里都有!太强了!

  • 大家好,我是你们的好朋友大明,欢迎大家来到【大明青年】。今日分享大家好。每个人需要的资源都不一样,有人要音乐软件?要看小说软件?要看动漫软件?今天就推荐一款软件,可以满足所有
  • 永远不要在你的 Linux 系统上运行这些命令

  • 点击上方蓝字 ● 关注Linux公社 本文涉及到一些你绝不能在你的 Linux 系统上运行的命令,因为它们可能对你的 Linux 系统造成致命的影响。因此,在我继续之前,我想指出,本文仅
  • 国内IT软件外包公司汇总(2023最新版)

  • 作者:沉默王二Java 程序员进阶之路:https://tobebetterjavaer.com大家好,我是二哥呀。上次在文章留言区做了一个小小的统计:点赞人数中,外包和不在外包的比例,大家可以感受下。这
  • 分库分表,可能真的要退出历史舞台了!

  • 「 关注“石杉的架构笔记”,大厂架构经验倾囊相授 」文章来源:【公众号:小姐姐味道】即使是不懂编程的玩家,在对比 NAS 的时候,也会两眼放光,考虑很多因素,比如 RAID 级别、速度、
  • 值得收藏:一份非常完整的 MySQL 规范~

  • 原价199元,现在参加拼团活动立享优惠价仅 99 元,赶快一起参团吧!《从零开始带你成为MySQL实战优化高手》无论在面试还是工作中,MySQL都是所有后端程序员必须熟练掌握的技术,而目