尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

Java并发三剑客:volatile、synchronized、CAS核心原理与实战

发布时间:2026/9/24 22:35:10

资讯中心
01
ARTICLE

Java并发三剑客:volatile、synchronized、CAS核心原理与实战

Java并发三剑客:volatile、synchronized、CAS核心原理与实战
大家好我是你们的老朋友一个写了快十年Java、也面了不下两百人的老开发。今天想跟各位聊一个Java面试里永远绕不开、几乎每轮技术面都会出现的话题synchronized、volatile和CAS。很多同学背八股文时能把三者的概念背得滚瓜烂熟——“volatile保证可见性synchronized保证原子性CAS是乐观锁”——但真到了面试官追问一句“那你说说你们项目里到底哪里用到了CAS为什么这里不能用volatile”的时候立马就卡壳了。这其实是典型的“只背了结论没理解本质”。这篇文章我不想再把教科书上的定义抄一遍而是想从一个面试官的角度从原理到场景掰开揉碎讲清楚这三者到底是什么、为什么这么设计、它们各自都“能干什么”和“不能干什么”。无论你是准备校招的应届生还是想巩固底层知识点的职场新人或者是面试前想临时抱佛脚的老兵这篇文章的思路应该都能帮你把这块拼图补完整。1. 先把基本盘搞清楚并发编程要解决的三大问题在聊synchronized、volatile、CAS之前我们必须先达成一个共识这三个东西本质上都是Java给我们提供的“线程安全”工具。而“线程安全”这个词背后其实对应的是并发编程里的三个老大难问题——原子性、可见性、有序性。如果你不理解这三个问题后面所有的讨论都是空中楼阁。1.1 原子性操作要么全做要么全不做原子性简单说就是一个操作或者多个操作要么全部执行成功要么全部执行失败中间不能被任何因素打断。就好比银行转账A账户扣100块B账户加100块这必须是一个不可分割的整体。如果在扣款成功、入账失败的情况下系统就返回了那这钱就凭空消失了谁也不能接受。在单线程环境下CPU是按顺序执行的原子性天然得到保证。但多线程环境下CPU调度是会随时切换线程的。切换的本质就是保存当前线程的执行上下文让另一个线程来占用CPU。这个时候如果一个线程在执行“读-改-写”的过程中被切走了另一个线程读到旧数据再写回时就把别人的修改覆盖了原子性就被破坏了。i就是一个最经典的例子。很多人觉得i是一条语句天然就是原子的其实不然。它在字节码层面会被拆成三步先读取 i 的值然后把 i 加1最后把新值写回。这三步之间任何一步都可能被线程切换打断。1.2 可见性一个线程的修改别的线程能立刻看见吗可见性问题的根源是CPU的多级缓存架构。为了提高执行效率CPU不会每次都直接跟内存打交道而是在CPU和内存之间加了好几层高速缓存。线程执行计算时会先把变量从内存复制到CPU缓存中改完之后再择机刷新回主内存。问题就出在这个“择机”上。Java内存模型JMM规定线程对共享变量的修改先更新到自己的工作内存可以理解为CPU缓存的一个抽象这个更新对其他线程不一定是立即可见的。用大白话说就是一个线程改了值另一个线程拿到的可能还是改之前的旧值。这就是可见性问题的核心一个线程对共享变量的修改什么时候、以什么方式对另一个线程可见是不确定的。你可能会说这也太不靠谱了吧但没办法CPU为了性能牺牲了部分一致性而这个“漏洞”就得靠语言层的机制来堵。1.3 有序性代码执行顺序真的和写的顺序一样吗第三个问题是有序性。为了提升执行效率编译器和CPU会对代码指令进行重排序把没有数据依赖关系的指令重新排列以更好地利用CPU流水线。在单线程下重排序不会影响最终结果因为最终结果和顺序执行是一模一样的。但多线程下重排序就可能造成诡异的问题。这里我需要举一个很经典的双重检查锁Double-Checked Locking例子。你写了三行代码instance new Singleton();这行代码看似简单但背后其实对应着多个步骤分配内存、初始化对象、把引用赋值给变量。如果编译器和CPU把这几个步骤重排序成“分配内存、把引用赋值、再初始化对象”那么当线程A执行到“把引用赋值”这一步时线程B恰好进来检查发现instance ! null直接拿去用就会拿到一个还没初始化完成的对象一调用里面的方法就空指针了。所以有序性问题在单线程下无伤大雅但在多线程共享变量的场景下就可能导致致命的逻辑错误。现在我们把这三个问题列个表大家有个直观印象问题通俗解释后果原子性操作被打断执行了一半就切走了数据不一致、覆盖写可见性改了但别人看不见读到旧值死循环、判断失效有序性指令被重排执行顺序和代码不一致对象未初始化就被使用volatile、synchronized和CAS每一个都是冲着这三座大山来的但它们的定位、武器和适用场景完全不一样。接下来逐个拆解。2. volatile最轻量的线程协作机制volatile是Java里最轻量的“线程同步”工具也是面试中被问得最多、误解也最多的一个关键字。很多人只知道它“保证可见性”却忽略了它“不保证原子性”这个致命短板。我们先看看它到底管用在哪。2.1 volatile的两个核心语义可见性 有序性禁重排volatile从JMM层面来说有两层核心语义第一保证可见性。当一个变量被声明为volatile对于这个变量的读和写JMM会要求线程在操作时必须直接从主内存中读取写的时候也必须直接刷新到主内存。这就相当于告诉Java虚拟机“这个变量是大家共享的别让线程各自在本地缓存里瞎搞都给我实打实地读写主内存。”第二禁止指令重排序。volatile会在读写操作前后插入内存屏障Memory Barrier告诉CPU和编译器这个变量的读写顺序不能随便乱排要按代码里写的来。这就保证了第二小节里那个单例场景的安全性——用volatile修饰单例实例引用初始化步骤就不会被重排序到赋值之后。从底层实现来看volatile写操作会在后面插入一个StoreStore屏障和StoreLoad屏障读操作前面会插入LoadLoad屏障和LoadStore屏障。不过这些底层细节面试时不一定需要背到这么深但你要能理解它通过“强行读写主内存 插入屏障”这两招同时解决了可见性和有序性两个问题。2.2 volatile的最佳实践状态标记与安全发布那volatile到底适合用在哪些场景根据我自己的实践经验最典型的有三类场景一状态标志位。这是最经典、也最安全的用法。比如用一个boolean变量作为开关控制线程是否继续执行。public class ThreadStopExample { private volatile boolean running true; public void stop() { running false; } public void doWork() { while (running) { // 业务逻辑 } } }这里的running被一个线程修改被另一个线程循环读取。如果不加volatile修改线程改了running false读线程很可能一直读到旧值永远跳不出循环。加了volatile修改后能立即被读线程看到问题就解决了。这个场景下对running的操作只是简单的布尔赋值具有原子性配合volatile的可见性完美契合。场景二双重检查锁DCL的单例模式。前面提到过new Singleton()不是一个原子操作如果没有volatile防止重排序一个线程可能拿到一个半初始化的对象。加上volatile是最正确的解法。场景三对变量的操作本身是原子操作但需要保证可见性。比如一个volatile long或者volatile double因为64位的写操作在32位JVM上可能被拆成两次32位写加volatile能保证其原子性。不过这个场景现在不太常见了但作为知识面了解即可。2.3 volatile的致命短板它不保证原子性这里我要重点强调一个面试中极其高频的追问点为什么volatile不能替代synchronized因为volatile只管可见性和有序性它管不了原子性。我们回到volatile int count多个线程同时执行count。看起来有volatile保证了可见性但别忘了count是“读-改-写”三步操作。线程A读到count1线程B也读到count1因为A还没写回然后A写回2B也写回2最终结果明明是2我们期望的却是3。这就是典型的“丢了更新”。更坑的是volatile的可见性在这里反而会造成一种假象每次赋值之后值会立刻刷新到主内存看起来好像“同步”了但实际上中间读取的那一步是有时间窗口的。所以面试官最喜欢问的那个问题来了——“volatile能保证原子性吗”答案必须斩钉截铁不能。提示请记住一个判断口诀——如果对变量的操作是复合操作读-改-写那就不能依赖volatile保平安。3. synchronized从“重量级锁”到“轻量级锁”的进化如果说volatile是一把轻巧的手术刀那synchronized就是一把功能全面的瑞士军刀。它能同时解决原子性、可见性和有序性是Java并发编程里最基础也最可靠的同步手段。3.1 synchronized怎么做到“全都要”的synchronized的底层依托于Monitor锁监视器锁。当一个线程进入synchronized代码块时它会尝试获取Monitor的所有权其他线程只能阻塞等待。这样就保证了代码块的互斥性——同一时刻只有一条线程在跑不可能出现指令交错原子性自然就保证了。至于可见性synchronized的规定是线程在解锁前必须把共享变量的最新值刷新到主内存加锁时则清空工作内存中的共享变量强制从主内存中重新读取。用大白话说进锁和出锁之间自动把可见性问题也处理掉了。而有序性方面获取锁进入临界区和释放锁天然形成了两个内存屏障锁内代码的重排序被限制在一个很严格的范围内。所以它一个工具同时搞定三座大山非常省心。不过省心的代价在JDK 1.6之前是“性能差”。因为早期的synchronized是一上来就申请操作系统级别的重量级互斥量Mutex线程阻塞和唤醒涉及到操作系统内核态和用户态的切换成本非常高。所以很多人谈锁色变于是就有了后来那一堆优化。3.2 锁升级偏向锁、轻量级锁、重量级锁JDK 1.6 引入了一波重要的锁优化让synchronized从“傻大黑粗”变成了“按需升级”的聪明锁。它不再是一上来就找操作系统要锁而是有一个升级路径无锁 - 偏向锁 - 轻量级锁 - 重量级锁。偏向锁如果一个线程反复进入同一个同步块它会在对象头里记录下持有锁的线程ID。下次这个线程再进来时只需要检查一下标记对不对对就直接通过连CAS都不用。用大白话说就是——这个锁“偏心”于第一个获得它的线程因为这个线程大概率会反复进来。偏向锁的出现是针对“只有一个线程访问同步块”这种常见场景做的优化。轻量级锁如果第二个线程也来竞争了偏向锁会撤销并升级为轻量级锁。这个锁其实并不真正阻塞其他线程而是通过CAS自旋来抢锁。抢不到的人就在那里空转CPU拼命尝试。自旋的本质是“用CPU时间换线程上下文切换的开销”。重量级锁如果自旋也抢不到比如锁竞争非常激烈或者自旋次数太多就会升级为重量级锁真正进入操作系统的Monitor阻塞队列等待唤醒。这套升级设计的精妙之处在于它让synchronized在不同竞争激烈程度下都能找到一个性价比合适的锁形态。面试时如果能把这条升级路径讲清楚绝对是一个大加分项。3.3 synchronized的适用场景与优势那实际项目中什么时候优先选synchronized我的经验是当你对代码逻辑的原子性有严格要求、锁的范围又不算太长时synchronized是最稳妥的选择。比如在库存扣减场景里你一定不希望出现“并发超卖”那用一个synchronized方法把检查库存、扣减库存包围起来就是最直观的做法。它的核心优势非常明显自动释放锁。无论是正常执行完还是抛出异常JVM都会自动释放Monitor锁。这一点比手写Lock要放心太多不用记得finally里unlock。可重入。同一个线程可以多次进入同一个锁不会自己把自己锁死。这种“可重入性”在递归调用场景中特别重要。原子性、可见性、有序性一步到位。不需要额外搭配volatile不容易写错。4. CAS无锁并发的“硬核乐观派”聊完两块“锁”终于到CAS了。CAS全称 Compare And Swap也就是“比较并交换”。它不是锁但它是很多无锁并发工具比如AtomicInteger、ConcurrentHashMap的分段操作的底层基石。4.1 CAS的实现原理CPU级别的原子指令CAS的核心思路很简单先比较再交换。它的操作逻辑是——只有当内存中的当前值 V 等于预期值 A 时才把内存中的值更新为新值 B否则什么都不做。翻译成人话我去改一个值之前先递个眼神给内存“老哥你现在是不是1如果是我就改成2如果不是说明别人已经改过了我就不动。”这个“比较交换”的动作在Java中是通过Unsafe类里提供的本地方法最终调用了CPU底层的原子指令比如x86平台的cmpxchg完成的。因为是CPU硬件指令级别的操作所以它天然就是原子的不会被线程切换打断。在Java的java.util.concurrent.atomic包里AtomicInteger、AtomicLong、AtomicReference这些类底层核心都是CAS。比如AtomicInteger的incrementAndGet就是在一个死循环里不断尝试 CASpublic final int incrementAndGet() { for (;;) { int current get(); int next current 1; if (compareAndSet(current, next)) { return next; } } }第一次读到 current1如果此刻别人没有修改我改成2就成功了如果别人在我读完之后改了compareAndSet 失败代码就重新循环再读一次新的 current再尝试。这种“失败就重试”的策略就是自旋。这么写的好处是线程不需要阻塞、不需要操作系统介入、没有上下文切换开销。所以 CAS 又被称为“无锁并发”或者“乐观锁”——它乐观地认为竞争很少所以不提前设防大不了失败再来。4.2 CAS的三大“坑”ABA、自旋开销、只能作用单个变量CAS 听起来很美但它绝不是银弹。面试官最喜欢在CAS后面连环追问这几个问题问题一ABA问题。这是CAS最著名的天坑。想象一个经典的场景线程A读到内存值是1准备改成2但在此之前线程B已经把1改成了3又改回了1。在线程A看来内存值从头到尾都是1它不知道发生过多轮修改于是CAS成功但它基于的“过去状态”其实已经被篡改过了。拿现实打比方你有一张面值100块的人民币你A去洗衣服前把它锁在抽屉里洗衣服回来发现抽屉里的钱还是100块你以为没动过。但实际上你老婆线程B拿这100块去买菜又用另外的100块换了回来。你只看了数额没看编号。解决ABA问题的标准方案是加版本号或者用AtomicStampedReference。它可以让每次修改都带一个版本号CAS比较时不仅比较数值还比较版本号这样就能识别出“值没变但版本变了”的情况。问题二自旋带来的CPU开销。CAS失败后线程不是休眠而是死循环重试。如果并发量极高、竞争极其激烈大量线程都在空转CPU反而会拖垮性能。这是CAS一个天生的缺点。所以实际项目中CAS更适合用在竞争不那么激烈的场景或者原子操作内部只做一个“小动作”的场景。问题三只能保证单个共享变量操作的原子性。CAS只是对一个变量的比较交换是原子的多个变量之间的复合操作CAS没法保证整体的原子性。比如“先扣库存再记流水”这种跨多个变量的操作CAS就无能为力了。这种情况下还是得老实点上锁。5. 三者对比与应用场景选择面试官想听什么样的答案前面四部分已经把三者的原理和优缺点都理了一遍下面把它们放在一张表里做个终极对比这个表也是我面试时心里默认的评分表你能把它讲清楚这题就问题了。对比维度volatilesynchronizedCAS解决核心问题可见性、有序性可见性、有序性、原子性原子性单变量是否阻塞不阻塞阻塞可升级不阻塞自旋开销低低到高看竞争竞争激烈时可能很高典型场景状态标志、单例DCL、一写多读读写竞争激烈、复合操作、需要代码块互斥原子计数器、乐观并发控制常见坑不保证原子性使用不当易死锁/性能差ABA问题、CPU空转、不支持多变量实现本质内存屏障 主内存读写Monitor监视器锁CPU的cmpxchg指令那么如何在实际项目里选型呢我自己一般按下面这个思路来判断如果只是一个布尔状态开关多个线程都只是读偶尔一个线程写用volatile零成本。如果是复合操作或者一坨代码要互斥执行比如修改库存、转账、对账用synchronized是最直观的。如果只是一个计数器累加而且并发量可控优先考虑AtomicInteger这类CAS工具类无锁无阻塞性能很好。如果多个线程读多写少并且对数据实时性要求很高考虑volatile 不可变对象。不过一定要警惕过度设计。很多新手喜欢动不动就用ConcurrentHashMap、用AtomicReference去搞一些花活结果复杂度飙升出了问题还不好排查。我的原则是能用简单方案解决就是好方案。绝大多数业务系统里synchronizedvolatile已经能覆盖90%以上的场景了。6. 面试场上怎么把这些点答成加分项最后再聊聊家人们最关心的“面试”本身。越底层的东西越考一个人的基本功。怎么答才是从“背概念”到“有理解”的差距我分享几个我面试时经常用的提问组合。6.1 一个值得一提的独门答题思路面试官问“谈谈synchronized和volatile的区别”时很多人的回答是“synchronized是锁volatile是轻量级同步机制。”这样答当然对但太单薄没有层次感。我建议可以试着用“功能范围”作为线索来讲会显得你理解得比较透彻先回答共同点两者都能保证共享变量的可见性和有序性。再回答本质区别synchronized还能保证原子性volatile不能。synchronized的本质是互斥访问volatile的本质是强制读写主内存。接着讲应用场景差异volatile适合“一写多读”和“状态标志”synchronized适合“多写多读、复合操作”。最后补一句关键题眼volatile不能完全替代synchronized因为一句话解释——它管不了复合操作的原子性。这样一套组合拳打下来面试官会觉得你不是在背题而是在讲一个你真正用过的工具。6.2 CAS与锁的关系一个有深度的隐藏考点关于CAS和锁的关系我自己面试时还会追问一个点Java里很多锁的实现底层其实也用到了CAS。比如synchronized升级为轻量级锁时抢锁的过程就是一个CAS操作ReentrantLock内部的AQS头部节点变更也是CAS实现的。所以CAS和锁并不是完全对立的关系而是互相配合的锁是一种宏观策略CAS是一种底层原语。把这一层关系讲出来你的回答就超越90%的候选人了因为它表明你看到了语言层和硬件层之间的桥梁。6.3 真实工作里最常见的踩坑记录内含避坑方法说完了面试再唠几句工作里实际会踩的坑。踩坑一以为 volatile 能扛住并发计数。之前有同事在活动里用volatile int做秒杀人数统计压测一上就崩。原因就是2.3节说的复合操作原子性。后来换成了AtomicInteger几十万的并发量轻松扛住。踩坑二synchronized 锁范围过大导致性能雪崩。有人为了保护一个局部小变量直接把整个方法都加锁结果这个方法每秒被调用十万次所有请求全被堵住了。正确的做法是缩小锁的粒度只在真正需要互斥的几行代码上用synchronized。踩坑三CAS自旋导致CPU飙到100%。某个服务用了AtomicBoolean做全局开关但因为一个异常导致CAS一直失败进入了无限自旋CPU直接被打满。排查问题时第一反应就是看线程栈发现一堆线程卡在compareAndSet的死循环里。这个问题的教训是——CAS真的不适合长时间自旋如果重试次数太多最好加个重试上限或者退回用锁。这些坑都不是什么高深的大道理但实际踩一次会记一辈子。也希望今天的分享能帮你们少踩一次那就是值得的。我个人在实际使用中最推崇的其实还是这几种简单方案按需组合。毕竟并发编程里能用简单的并发原语解决的问题尽量不要引入复杂的中间件和分布式锁。把你的synchronized、volatile、CAS用明白、用到位这个基本功比任何花哨的框架都值钱。
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。