还在盲信无锁编程?你知道 CAS 机制背后的 ABA 黑洞能吞掉你多少年终奖吗?

举报
喵手 发表于 2025/12/08 20:31:39 2025/12/08
【摘要】 开篇语哈喽,各位小伙伴们,你们好呀,我是喵手。运营社区:C站/掘金/腾讯云/阿里云/华为云/51CTO;欢迎大家常来逛逛  今天我要给大家分享一些自己日常学习到的一些知识点,并以文字的形式跟大家一起交流,互相学习,一个人虽可以走的更快,但一群人可以走的更远。  我是一名后端开发爱好者,工作日常接触到最多的就是Java语言啦,所以我都尽量抽业余时间把自己所学到所会的,通过文章的形式进行输出,...

开篇语

哈喽,各位小伙伴们,你们好呀,我是喵手。运营社区:C站/掘金/腾讯云/阿里云/华为云/51CTO;欢迎大家常来逛逛

  今天我要给大家分享一些自己日常学习到的一些知识点,并以文字的形式跟大家一起交流,互相学习,一个人虽可以走的更快,但一群人可以走的更远。

  我是一名后端开发爱好者,工作日常接触到最多的就是Java语言啦,所以我都尽量抽业余时间把自己所学到所会的,通过文章的形式进行输出,希望以这种方式帮助到更多的初学者或者想入门的小伙伴们,同时也能对自己的技术进行沉淀,加以复盘,查缺补漏。

小伙伴们在批阅的过程中,如果觉得文章不错,欢迎点赞、收藏、关注哦。三连即是对作者我写作道路上最好的鼓励与支持!

0. 前言:那该死的“并发”噩梦

兄弟们,咱们干后端的,谁没半夜被并发 Bug 吓醒过?😱 尤其是当你发现数据库里的钱数不对,或者库存莫名其妙超卖的时候,那种后背发凉的感觉,简直比相亲遇到前女友还尴尬。

为了追求极致的性能(KPI),我们把 synchronized 这种重型锁扔进了垃圾桶,拥抱了传说中的“无锁编程”。于是,CAS(Compare And Swap)成了我们的救世主。大家都说它快,说它好,说它是并发界的“瑞士军刀”。🔪

但是!Too young, too simple! 😤 你以为 CAS 就是银弹?哼,它要是疯起来,连它自己都怕!特别是那个阴魂不散的 ABA 问题,它就像是那种看着老实巴交、实则一肚子坏水的“隐形杀手”。今天,老哥我就带你扒开 CAS 的外衣,看看它到底有多性感,又有多危险!💡

1. CAS 到底是何方神圣?(别背书,听人话)

咱们先不整那些晦涩的术语。CAS,全称 Compare And Swap(比较并交换)。听着挺高大上,其实说白了,它就是个死心眼的倔驴。🐴

想象一下,你要去抢购一台 iPhone 16(别问为啥不是 15,咱们要有前瞻性)。

传统的 synchronized 锁就像是:商场只有一个柜台,你进去了,把门反锁,慢悠悠地买,外面几千人在风中凌乱排队。安全是安全,但效率低到令人发指!🐢

CAS 是怎么玩的呢?它不锁门。它就像是你带着两个小纸条冲到柜台前:

  1. 纸条 V (Value):柜台里现在的库存(内存里的实际值)。
  2. 纸条 A (Expect):你以为的库存(旧的预期值)。
  3. 纸条 B (New):你买完后剩下的库存(想修改的新值)。

CAS 的逻辑是这样的:

“嘿!柜员!我看一眼(Compare),如果现在的库存 V 等于我预期的 A,说明没人插队抢货,那你赶紧把库存改成 B(Swap)!如果 V 不等于 A,说明哪个孙子刚才手快抢走了,那我就不算数了,我重试(自旋)或者认怂!”

看懂没?这就叫乐观锁!它总是假设“没人跟我抢”,真有人抢了再哭着重来。😂

我们来看一眼 Java 里 AtomicInteger 的底层实现,这玩意儿就是 CAS 的亲儿子:

// Java Implementation of AtomicInteger (Simplified)
public final int getAndIncrement() {
    // 这里的 this 是当前对象,valueOffset 是内存地址偏移量,1 是要加的数
    // 这是一个死循环(自旋),直到成功为止!
    for (;;) {
        int current = get(); // 拿到当前的 A
        int next = current + 1; // 计算出想要的 B
        //以此为誓,若内存里的值还是 current,就给老子换成 next!
        if (compareAndSet(current, next)) 
            return current;
    }
}

这一波操作,直达 CPU 汇编指令 cmpxchg,这是硬件级别的原子性支持!这就叫专业! 👊


2. ABA 问题:当你以为前女友没谈恋爱…

这才是今天的重头戏!CAS 看起来很完美对吧?效率高,不用上下文切换。但是,它有一个致命的逻辑漏洞,就是 ABA 问题。

啥是 ABA? 听我给你讲个狗血的故事:🐶

你桌上放了一杯水(状态 A)。
  你去上了个厕所。
  这时候,你那个损友过来,把水喝干了(状态 B),然后觉得心里过意不去,又接了一杯一模一样的水放回去(状态 A)。
  你上完厕所回来,看了一眼:“哟,水还在(A == A),没人动过!” 于是你端起来一口闷了。🤢

这就是 ABA 问题! 仅仅比较值是不是相等,是无法判断这个过程是否被“偷梁换柱”过的!

你可能会说:“喝杯水怎么了?又不死人。”
  嘿嘿,如果在链表(Stack)操作里,这能让你程序直接原地爆炸!💥

💥 真实惨案现场(链表版)

假设咱们有个无锁栈(Stack),里面有两个节点:Top -> A -> B
  线程 1 想要把 A 弹出来(Pop),它的期望是:CAS(Top, A, B)。也就是如果 Top 还是 A,就把 Top 指向 B。
  就在线程 1 准备执行 CAS 还没执行的那一瞬间,线程 1 挂起了(被系统调度走了)。 ⏸️

这时候,线程 2 也就是那个捣乱的损友冲进来了:

  1. 它把 A 弹出来了。
  2. 它把 B 也弹出来了。
  3. 它又把 A 压回去了!
  4. 现在的栈结构是:Top -> A (注意,B 已经没了!A 后面可能是 null 或者垃圾数据)。

线程 1 醒了:
  它看了一眼 Top,“哇!还是 A 耶!看来一切正常!”
  于是它自信地执行了 CAS,把 Top 指向了 B。
  可是 B 早就被线程 2 删掉了啊!B 现在的内存地址可能已经是悬空的,或者被重用成了别的东西! 😱

这就导致了传说中的野指针或者数据错乱。你的程序就像脱缰的野马,奔着内存泄漏和崩溃就去了。这就问你怕不怕?💀


3. 解决方案:给你的爱加上“时间戳”

既然单纯比对“值”不可靠,那咱们就得加码!就像给猪肉盖章一样,咱们得给数据盖个版本号(Version)

这就是解决 ABA 问题的核心思路:版本号机制

原本是:A -> B -> A (CAS 觉得 OK)
  现在变成:1A -> 2B -> 3A
  这时候 CAS 一看:“卧槽?虽然值还是 A,但版本号从 1 变成 3 了?这明显被人动过手脚啊!” 🚫 拒绝执行!

🛠️ 实战代码:AtomicStampedReference

在 Java 里,JDK 的大佬们早就给咱们准备好了工具:AtomicStampedReference。这名字听着就贵气,自带“邮戳(Stamp)”。

来,咱们看段代码,这可是能直接跑的干货,别眨眼!👀

import java.util.concurrent.atomic.AtomicStampedReference;

public class ABASolutionDemo {

    // 初始值是 100,初始版本号是 1
    static AtomicStampedReference<Integer> atomicStampedRef = 
        new AtomicStampedReference<>(100, 1);

    public static void main(String[] args) {
        
        // 线程 1:那是那个捣乱的损友,模拟 ABA 操作
        new Thread(() -> {
            int stamp = atomicStampedRef.getStamp();
            System.out.println("Thread 1 - Initial Stamp: " + stamp);
            
            try { Thread.sleep(100); } catch (InterruptedException e) {}

            // 第一次修改:100 -> 101
            atomicStampedRef.compareAndSet(100, 101, 
                atomicStampedRef.getStamp(), atomicStampedRef.getStamp() + 1);
            
            // 第二次修改:101 -> 100 (这就构成了 ABA!)
            atomicStampedRef.compareAndSet(101, 100, 
                atomicStampedRef.getStamp(), atomicStampedRef.getStamp() + 1);
            
            System.out.println("Thread 1 - ABA operation done. Current Stamp: " 
                + atomicStampedRef.getStamp());
        }).start();

        // 线程 2:这是蒙在鼓里的你
        new Thread(() -> {
            int stamp = atomicStampedRef.getStamp(); // 刚开始拿到的版本号是 1
            int reference = atomicStampedRef.getReference(); // 刚开始拿到的值是 100
            System.out.println("Thread 2 - Initial Stamp: " + stamp);

            // 故意睡得比线程 1 久,让线程 1 有足够时间搞事情
            try { Thread.sleep(200); } catch (InterruptedException e) {} 

            // 此时内存里的值虽然是 100,但版本号已经是 3 了!
            // 咱们手里拿的版本号还是 1,所以这里 CAS 绝对会失败!
            boolean result = atomicStampedRef.compareAndSet(
                reference,          // 期望值 100
                2024,               // 新值 2024
                stamp,              // 期望版本号 1
                stamp + 1           // 新版本号 2
            );

            System.out.println("Thread 2 - Update Success? " + result); 
            // 结果必然是 false!这就是版本号的威力!🛡️
            
            if (!result) {
                System.out.println("Thread 2: Damn! Someone touched my data! 😡");
                System.out.println("Current actual stamp is: " + atomicStampedRef.getStamp());
            }
        }).start();
    }
}

看到了吗? 虽然值回到了 100,但因为 stamp 对不上,线程 2 的操作被拦下来了,避免了可能发生的逻辑错误。这就是技术的魅力啊兄弟们!✨


4. 深度拓展:除了 Java,咱们还能在哪里秀?

别以为 CAS 只是 Java 的面试题,这思想在计算机科学里到处都是!🌍

数据库领域的“乐观锁”

你在写 SQL 的时候,是不是经常处理库存扣减?
  如果你直接 UPDATE goods SET stock = stock - 1 WHERE id = 1,并发高了数据库容易死锁。
  聪明人都怎么干?加个 version 字段!

UPDATE goods 
SET stock = stock - 1, version = version + 1 
WHERE id = 1 AND version = old_version;

这就是数据库层面的 CAS + ABA 解决方案!如果返回的影响行数是 0,说明被别人插队了,报错给前端或者重试。这就把压力从数据库锁转移到了业务逻辑上,高并发系统的基石啊! 🏗️

自旋的代价(别光看好的)

虽然 CAS 好,但它有个坏毛病:自旋(Spin)
  如果并发冲突太厉害,比如几千个线程同时抢一个变量,大部分线程都会处于 while(true) 的死循环里空转。这时候你的 CPU 占用率会直接飙到 100%,风扇转得跟直升机一样响!🚁
  所以,CAS 适合“读多写少”或者“冲突概率低”的场景。要是冲突高,老老实实加 synchronized 或者 ReentrantLock 吧,别硬撑,硬撑容易过劳肥。🐷

5. 写在最后:别做“全栈”,要做“全懂”

咱聊了这么多,其实核心就一句话:没有什么技术是完美的,只有最适合场景的技术。

CAS 给了我们高性能的可能,ABA 问题又给我们挖了个大坑,而 AtomicStampedReference 给了我们填坑的铲子。这就是程序员的宿命——一直在填坑的路上狂奔。🏃‍♂️

下次面试官再问你:“CAS 有什么问题啊?怎么解决啊?”
  你嘴角上扬,把这篇文章里的例子也就是那个“喝水”的梗扔出去,再反手写个带版本号的代码,我敢保证,面试官看你的眼神都会变得含情脉脉!😍

好了,今天就扯到这儿。代码敲累了记得起来活动活动,毕竟咱们的腰间盘比 CAS 机制脆弱多了!加油吧,打工人!💪✨

… …

文末

好啦,以上就是我这期的全部内容,如果有任何疑问,欢迎下方留言哦,咱们下期见。

… …

学习不分先后,知识不分多少;事无巨细,当以虚心求教;三人行,必有我师焉!!!

wished for you successed !!!


⭐️若喜欢我,就请关注我叭。

⭐️若对您有用,就请点赞叭。
⭐️若有疑问,就请评论留言告诉我叭。


版权声明:本文由作者原创,转载请注明出处,谢谢支持!

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。