如果面试官问你CAS,你还这么答,可能就要回去等通知了

本文介绍了CAS(Compare-And-Swap)的概念,指出它是CPU并发原语,用于保证线程安全。通过Java的Unsafe类展示了CAS的使用,并分析了其底层汇编实现。文章还讨论了CAS的缺点,包括循环时间长、ABA问题,并提供了AtomicInteger的getAndIncrement()方法源码解析,帮助读者理解其原子性操作。最后,提醒读者深入理解并发原理以应对面试挑战。

前言

大家好,我是JAVA高级开发之路,一个总在为粉丝解决面试题的程序员。

最近有几个粉丝说在面试面试中遇到了CAS的问题,连着几次面试都没有让面试官满意,区区CAS底层源码,怎能难倒咱们这届程序员们呢?都支棱起来,跟我一起来搞定CAS底层源码

什么是CAS

CAS的全称是Compare-And-Swap,它是CPU并发原语。

它的功能是判断内存某个位置的值是否为预期值,如果是则更改为新的值,这个过程是原子的

CAS并发原语体现在Java语言中就是sun.misc.Unsafe类的各个方法。调用UnSafe类中的CAS方法,JVM会帮我们实现出CAS汇编指令,这是一种完全依赖于硬件的功能,通过它实现了原子操作,再次强调,由于CAS是一种系统原语,原语属于操作系统应用范畴,是由若干条指令组成,用于完成某个功能的一个过程,并且原语的执行必须是连续的,在执行过程中不允许被中断,也就是说CAS是一条CPU的原子指令,不会造成所谓的数据不一致的问题,也就是说CAS是线程安全的。

我们来先看下这个代码:

如果面试官问你CAS,你还这么答,可能就要回去等通知了

如果面试官问你CAS,你还这么答,可能就要回去等通知了

大家猜一下结果是什么?

第二次打印会不会被赋值为 9999

上面代码的执行结果为:

如果面试官问你CAS,你还这么答,可能就要回去等通知了

如果面试官问你CAS,你还这么答,可能就要回去等通知了

那么为什么回事这样的结果?我们一起来看看中间的原理

这是因为我们执行第一个的时候,期望值和原本值是满足的,因此修改成功,但是第二次后,主内存的值已经修改成了6666,不满足期望值,因此返回了false,本次写入失败

如果面试官问你CAS,你还这么答,可能就要回去等通知了

如果面试官问你CAS,你还这么答,可能就要回去等通知了

这个就像是git的版本号一样,如果没有更改那么就可以提交,否则先把以前的pull下来合并之后再去提交;

CAS的底层原理

首先我们先看看 
atomicInteger.getAndIncrement()方法的源码

如果面试官问你CAS,你还这么答,可能就要回去等通知了

如果面试官问你CAS,你还这么答,可能就要回去等通知了

在这里细心的读者就发现了底层调用了一共unsafe类的getAndAddAInt方法。

我们去看看unsafee方法

如果面试官问你CAS,你还这么答,可能就要回去等通知了

如果面试官问你CAS,你还这么答,可能就要回去等通知了

Unsafe是CAS的核心类,由于Java方法无法直接访问底层系统,需要通过本地(Native)方法来访问,Unsafe相当于一个后门,基于该类可以直接操作特定的内存数据。Unsafe类存在sun.misc包中,其内部方法操作可以像C的指针一样直接操作内存,因为Java中的CAS操作的执行依赖于Unsafe类的方法。

注意Unsafe类的所有方法都是native修饰的,也就是说unsafe类中的方法都直接调用操作系统底层资源执行相应的任务。

为什么Atomic修饰的包装类,能够保证原子性,依靠的就是底层的unsafe类。

如果面试官问你CAS,你还这么答,可能就要回去等通知了

如果面试官问你CAS,你还这么答,可能就要回去等通知了

var5:就是我们从主内存中拷贝到工作内存中的值

那么操作的时候,需要比较工作内存中的值,和主内存中的值进行比较

假设执行 compareAndSwapInt返回false,那么就一直执行 while方法,直到期望的值和真实值一样

  • 如果相同,更新var5 + var4 并返回true
  • 如果不同,继续取值然后再比较,直到更新完成

  • val1:AtomicInteger对象本身
  • var2:该对象值得引用地址
  • var4:需要变动的数量
  • var5:用var1和var2找到的内存中的真实值用该对象当前的值与var5比较

这里没有用synchronized,而用CAS,这样提高了并发性,也能够实现一致性,是因为每个线程进来后,进入的do while循环,然后不断的获取内存中的值,判断是否为最新,然后在进行更新操作。

假设线程A和线程B同时执行getAndInt操作(分别跑在不同的CPU上)

1. AtomicInteger里面的value原始值为3,即主内存中AtomicInteger的 value 为3,根据JMM模型,线程A和线程B各自持有一份价值为3的副本,分别存储在各自的工作内存

2. 线程A通过getIntVolatile(var1 , var2) 拿到value值3,这是线程A被挂起(该线程失去CPU执行权)

3. 线程B也通过getIntVolatile(var1, var2)方法获取到value值也是3,此时刚好线程B没有被挂起,并执行了compareAndSwapInt方法,比较内存的值也是3,成功修改内存值为4,线程B打完收工,一切OK

4. 这是线程A恢复,执行CAS方法,比较发现自己手里的数字3和主内存中的数字4不一致,说明该值已经被其它线程抢先一步修改过了,那么A线程本次修改失败,只能够重新读取后在来一遍了,也就是在执行do while。

5. 线程A重新获取value值,因为变量value被volatile修饰,所以其它线程对它的修改,线程A总能够看到,线程A继续执行compareAndSwapInt进行比较替换,直到成功。

Unsafe类 + CAS思想:也就是自旋,自我旋转。

底层汇编

Unsafe类中的compareAndSwapInt是一个本地方法,该方法的实现位于unsafe.cpp中

  • 先想办法拿到变量value在内存中的地址
  • 通过Atomic::cmpxchg实现比较替换,其中参数X是即将更新的值,参数e是原内存的值

CAS缺点

CAS不加锁,保证一次性,但是需要多次比较

  • 但是对于多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候只能用锁来保证原子性

  • 循环时间长,开销大(因为执行的是do while,如果比较不成功一直在循环,最差的情况,就是某个线程一直取到的值和预期值都不一样,这样就会无限循环)
  • 只能保证一个共享变量的原子操作当对一个共享变量执行操作时,我们可以通过循环CAS的方式来保证原子操作
  • 引出来ABA问题?

ABA问题

总结

CAS

CAS是compareAndSwap,比较当前工作内存中的值和主物理内存中的值,如果相同则执行规定操作,否者继续比较直到主内存和工作内存的值一致为止

CAS应用

CAS有3个操作数,内存值V,旧的预期值A,要修改的更新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则不操作

对此,你有什么要补充的吗?欢迎在评论区留言。

写在最后

现在很多程序员们面试都是靠背面试题,这个只能适用于基础面,如果想要去大厂的话,一定要把钻研深度,这样才能在众多面试者中脱颖而出。

随着金九银十的结束,也有很多粉丝私聊小编,说自己拿到了大厂offer,同时小编再三请求下才让他们整理出了这份最新的互联网大厂面经

如果面试官问你CAS,你还这么答,可能就要回去等通知了

领取方式:点赞+收藏后,后台私聊小编:面经,即可免费领取


最后祝大家升职加薪,给个点赞+收藏,我们下次再见!

<think> 嗯,用户让我更详细、更白话地解释CAS,看来之前的解释还是太技术化了。用户提到"看懂",说明需要完全避免专业术语,用生活场景来类比。 用户可能是刚接触并发编程的开发者,或者正在准备面试。ta连续追CAS的实现方式,尤其关注"使用锁如何实现线程安全"这个场景,显然是在准备技术面试。需要强调CAS和锁的适用场景区别。 从对话历史看,用户已经读过我之前的CAS解释,但觉得够通俗。这次要彻底用生活化比喻: - 银行账户扣款例子最直观,保留但简化步骤 - 自旋锁伪代码虽然专业,但用户可能更需要一句话说明"它就像断刷新网页直到能提交" - 重点突出CAS的三个核心:比较、交换、重试 用户特别要求参考站内引用,注意到引用[1]的自旋锁实现和引用[4]的乐观锁定义最相关。需要把这些技术内容转化成: 1. 把自旋锁的while循环比喻成"断尝试抢车位" 2. 用"版本号"解释ABA题:就像发快递发现包装一样但里面东西被调包 3. 强调CPU指令是幕后功臣(但说CMPXCHG这种词) 最后结构化回,要把"面试官用锁怎么办"作为重点环节。需要明确给出决策树: - 低竞争场景 → CAS - 高竞争场景 → 虽然CAS能用但推荐LongAdder - 分布式环境 → 直接说明CAS适用 补充的Java代码示例保留,但加上注释说明每行对应生活场景中的哪一步。比如AtomicInteger.get()就是"先看一眼余额"。 </think> 好的,这个题非常关键!当面试官要求「使用锁实现线程安全」,**案是:CAS是实现无锁线程安全的核心技术,但不是唯一方案**。下面用白话逐步拆解: --- ### 一、为什么面试官这么面试官想考察: 1. 你是否理解「锁」和「无锁」的本质区别 2. 你是否掌握锁之外的线程安全方案 3. 你能否根据场景选择合适方案 > *就像用刀怎么切菜?”——你可以用剪刀、刨丝器,甚至手撕[^4]* --- ### 二、CAS 是首选方案(但非唯一) #### ✅ 核心原理:用CPU指令代替锁 ```java // Java中的CAS操作(原子类底层实现) public final boolean compareAndSet(int expect, int update) { return unsafe.compareAndSwapInt(this, valueOffset, expect, update); } ``` - **本质**:一条CPU指令(如x86的`CMPXCHG`)完成「读取→比较→修改」 - **无锁**:线程阻塞,通过**自旋重试**(类似反复确认) - **适用场景**:计数器、状态标记等简单操作[^1][^2] #### ✅ 典型应用 ```java // 示例:无锁计数器 AtomicInteger count = new AtomicInteger(0); void safeIncrement() { int oldVal; do { oldVal = count.get(); // 1. 读取当前 } while (!count.compareAndSet(oldVal, oldVal + 1)); // 2. CAS更新 } ``` > 相当于: > - 线程A:读余额100 → 试图改成101(成功) > - 线程B:读余额100 → 试图改成101(失败)→ **重试读最新101再改**[^2] --- ### 三、其他无锁方案(非CAS) #### 1️⃣ 线程封闭(Thread Confinement) - **原理**:直接把数据限定在单个线程内 ```java ThreadLocal<Integer> threadSafeCounter = ThreadLocal.withInitial(() -> 0); ``` - **场景**:Web服务器的用户请求计数(每个线程独立计数) #### 2️⃣ 可变对象(Immutable Objects) - **原理**:数据创建后永修改,自然线程安全 ```java public final class ImmutablePoint { private final int x; // final保证可变 private final int y; // 构造函数后x/y永改变 } ``` - **场景**:配置信息、坐标点等[^5] #### 3️⃣ 复制+提交(Copy-On-Write) - **原理**:修改复制整个数据集 ```java CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>(); // 写操作会复制新数组 ``` - **场景**:读多写少的黑白名单(如安全规则) --- ### 四、决策树:什么候用CAS? 根据场景选择技术: ```mermaid graph TD A[需要线程安全?] --> B{是否允许阻塞?} B -->|允许| C[无锁方案] C --> D{操作是否简单?} D -->|是| E[用CAS+自旋] D -->|否| F[用可变对象/CopyOnWrite] B -->|允许| G[用锁] ``` > **面试技巧**: > *“当避免锁,优先考虑CAS原子指令,特别是简单变量操作;若操作复杂或读多写少,可用可变对象或CopyOnWrite。比如计数器用`AtomicInteger`,配置管理用`ImmutableMap`。”* [^2][^5] --- ###
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值