ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

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

还在盲信无锁编程?你知道 CAS 机制背后的 ABA 黑洞能吞掉你多少年终奖吗? 全文目录开篇语0. 前言那该死的“并发”噩梦1. CAS 到底是何方神圣别背书听人话2. ABA 问题当你以为前女友没谈恋爱... 真实惨案现场链表版3. 解决方案给你的爱加上“时间戳”️ 实战代码AtomicStampedReference4. 深度拓展除了 Java咱们还能在哪里秀数据库领域的“乐观锁”自旋的代价别光看好的5. 写在最后别做“全栈”要做“全懂”文末开篇语哈喽各位小伙伴们你们好呀我是喵手。运营社区C站/掘金/腾讯云/阿里云/华为云/51CTO欢迎大家常来逛逛今天我要给大家分享一些自己日常学习到的一些知识点并以文字的形式跟大家一起交流互相学习一个人虽可以走的更快但一群人可以走的更远。我是一名后端开发爱好者工作日常接触到最多的就是Java语言啦所以我都尽量抽业余时间把自己所学到所会的通过文章的形式进行输出希望以这种方式帮助到更多的初学者或者想入门的小伙伴们同时也能对自己的技术进行沉淀加以复盘查缺补漏。小伙伴们在批阅的过程中如果觉得文章不错欢迎点赞、收藏、关注哦。三连即是对作者我写作道路上最好的鼓励与支持0. 前言那该死的“并发”噩梦兄弟们咱们干后端的谁没半夜被并发 Bug 吓醒过 尤其是当你发现数据库里的钱数不对或者库存莫名其妙超卖的时候那种后背发凉的感觉简直比相亲遇到前女友还尴尬。为了追求极致的性能KPI我们把synchronized这种重型锁扔进了垃圾桶拥抱了传说中的“无锁编程”。于是CASCompare And Swap成了我们的救世主。大家都说它快说它好说它是并发界的“瑞士军刀”。但是Too young, too simple! 你以为 CAS 就是银弹哼它要是疯起来连它自己都怕特别是那个阴魂不散的ABA 问题它就像是那种看着老实巴交、实则一肚子坏水的“隐形杀手”。今天老哥我就带你扒开 CAS 的外衣看看它到底有多性感又有多危险1. CAS 到底是何方神圣别背书听人话咱们先不整那些晦涩的术语。CAS全称 Compare And Swap比较并交换。听着挺高大上其实说白了它就是个死心眼的倔驴。想象一下你要去抢购一台 iPhone 16别问为啥不是 15咱们要有前瞻性。传统的synchronized锁就像是商场只有一个柜台你进去了把门反锁慢悠悠地买外面几千人在风中凌乱排队。安全是安全但效率低到令人发指而CAS是怎么玩的呢它不锁门。它就像是你带着两个小纸条冲到柜台前纸条 V (Value)柜台里现在的库存内存里的实际值。纸条 A (Expect)你以为的库存旧的预期值。纸条 B (New)你买完后剩下的库存想修改的新值。CAS 的逻辑是这样的“嘿柜员我看一眼Compare如果现在的库存 V 等于我预期的 A说明没人插队抢货那你赶紧把库存改成 BSwap如果 V 不等于 A说明哪个孙子刚才手快抢走了那我就不算数了我重试自旋或者认怂”看懂没这就叫乐观锁它总是假设“没人跟我抢”真有人抢了再哭着重来。我们来看一眼 Java 里AtomicInteger的底层实现这玩意儿就是 CAS 的亲儿子// Java Implementation of AtomicInteger (Simplified)publicfinalintgetAndIncrement(){// 这里的 this 是当前对象valueOffset 是内存地址偏移量1 是要加的数// 这是一个死循环自旋直到成功为止for(;;){intcurrentget();// 拿到当前的 Aintnextcurrent1;// 计算出想要的 B//以此为誓若内存里的值还是 current就给老子换成 nextif(compareAndSet(current,next))returncurrent;}}这一波操作直达 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也就是那个捣乱的损友冲进来了它把 A 弹出来了。它把 B 也弹出来了。它又把 A 压回去了现在的栈结构是Top - A注意B 已经没了A 后面可能是 null 或者垃圾数据。线程 1 醒了它看了一眼 Top“哇还是 A 耶看来一切正常”于是它自信地执行了 CAS把 Top 指向了 B。可是 B 早就被线程 2 删掉了啊B 现在的内存地址可能已经是悬空的或者被重用成了别的东西这就导致了传说中的野指针或者数据错乱。你的程序就像脱缰的野马奔着内存泄漏和崩溃就去了。这就问你怕不怕3. 解决方案给你的爱加上“时间戳”既然单纯比对“值”不可靠那咱们就得加码就像给猪肉盖章一样咱们得给数据盖个版本号Version。这就是解决 ABA 问题的核心思路版本号机制。原本是A - B - ACAS 觉得 OK现在变成1A - 2B - 3A这时候 CAS 一看“卧槽虽然值还是 A但版本号从 1 变成 3 了这明显被人动过手脚啊” 拒绝执行️ 实战代码AtomicStampedReference在 Java 里JDK 的大佬们早就给咱们准备好了工具AtomicStampedReference。这名字听着就贵气自带“邮戳Stamp”。来咱们看段代码这可是能直接跑的干货别眨眼importjava.util.concurrent.atomic.AtomicStampedReference;publicclassABASolutionDemo{// 初始值是 100初始版本号是 1staticAtomicStampedReferenceIntegeratomicStampedRefnewAtomicStampedReference(100,1);publicstaticvoidmain(String[]args){// 线程 1那是那个捣乱的损友模拟 ABA 操作newThread(()-{intstampatomicStampedRef.getStamp();System.out.println(Thread 1 - Initial Stamp: stamp);try{Thread.sleep(100);}catch(InterruptedExceptione){}// 第一次修改100 - 101atomicStampedRef.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这是蒙在鼓里的你newThread(()-{intstampatomicStampedRef.getStamp();// 刚开始拿到的版本号是 1intreferenceatomicStampedRef.getReference();// 刚开始拿到的值是 100System.out.println(Thread 2 - Initial Stamp: stamp);// 故意睡得比线程 1 久让线程 1 有足够时间搞事情try{Thread.sleep(200);}catch(InterruptedExceptione){}// 此时内存里的值虽然是 100但版本号已经是 3 了// 咱们手里拿的版本号还是 1所以这里 CAS 绝对会失败booleanresultatomicStampedRef.compareAndSet(reference,// 期望值 1002024,// 新值 2024stamp,// 期望版本号 1stamp1// 新版本号 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字段UPDATEgoodsSETstockstock-1,versionversion1WHEREid1ANDversionold_version;这就是数据库层面的 CAS ABA 解决方案如果返回的影响行数是 0说明被别人插队了报错给前端或者重试。这就把压力从数据库锁转移到了业务逻辑上高并发系统的基石啊️自旋的代价别光看好的虽然 CAS 好但它有个坏毛病自旋Spin。如果并发冲突太厉害比如几千个线程同时抢一个变量大部分线程都会处于while(true)的死循环里空转。这时候你的 CPU 占用率会直接飙到 100%风扇转得跟直升机一样响所以CAS 适合“读多写少”或者“冲突概率低”的场景。要是冲突高老老实实加synchronized或者ReentrantLock吧别硬撑硬撑容易过劳肥。5. 写在最后别做“全栈”要做“全懂”咱聊了这么多其实核心就一句话没有什么技术是完美的只有最适合场景的技术。CAS 给了我们高性能的可能ABA 问题又给我们挖了个大坑而AtomicStampedReference给了我们填坑的铲子。这就是程序员的宿命——一直在填坑的路上狂奔。‍♂️下次面试官再问你“CAS 有什么问题啊怎么解决啊”你嘴角上扬把这篇文章里的例子也就是那个“喝水”的梗扔出去再反手写个带版本号的代码我敢保证面试官看你的眼神都会变得含情脉脉好了今天就扯到这儿。代码敲累了记得起来活动活动毕竟咱们的腰间盘比 CAS 机制脆弱多了加油吧打工人✨… …文末好啦以上就是我这期的全部内容如果有任何疑问欢迎下方留言哦咱们下期见。… …学习不分先后知识不分多少事无巨细当以虚心求教三人行必有我师焉wished for you successed ⭐️若喜欢我就请关注我叭。⭐️若对您有用就请点赞叭。⭐️若有疑问就请评论留言告诉我叭。版权声明本文由作者原创转载请注明出处谢谢支持
返回列表