并发程序为什么会出问题很多人会先说“因为多线程同时执行”。这句话没错但太粗了。真正落到 Java 面试里通常要拆成三个词原子性、可见性、有序性。synchronized、Lock、volatile、CAS、Atomic 类本质上都是围绕这三个问题在做不同取舍。并发安全到底在防什么PPT 里把 Java 并发编程三大特性列得很清楚特性问题常见解决方式原子性一组操作执行到一半被别的线程插进来synchronized、Lock、CAS、Atomic 类可见性一个线程改了共享变量另一个线程看不到volatile、synchronized、Lock有序性编译器或 CPU 为优化执行顺序导致多线程结果异常volatile、锁、happens-before 规则并发程序出问题原子性可见性有序性共享变量操作被打断线程本地缓存没有及时同步指令重排改变多线程观察结果synchronized / Lock / CASvolatile / synchronized / Lockvolatile 内存屏障 / 锁语义原子性ticketNum-- 不是一步下面这种扣库存逻辑单线程下没问题多线程下就危险intticketNum10;publicvoidgetTicket(){if(ticketNum0){return;}System.out.println(Thread.currentThread().getName() 抢到一张票, 剩余:ticketNum);ticketNum--;}ticketNum--看起来是一行代码实际不是一个不可分割的动作。它至少包含读取ticketNum。计算ticketNum - 1。写回ticketNum。两个线程可能同时读到1然后都扣减成功。主内存 ticketNum线程 T2线程 T1主内存 ticketNum线程 T2线程 T1读取 ticketNum 1读取 ticketNum 1写回 0写回 0解决原子性最直接的方式是加锁publicsynchronizedvoidgetTicket(){if(ticketNum0){return;}ticketNum--;}也可以用Lock或者在适合的场景下用 Atomic 类底层的 CAS。JMM 是什么JMM全称 Java Memory ModelJava 内存模型。它不是 JVM 内存结构里的堆、栈、方法区那套东西。JMM 讨论的是多线程读写共享变量时Java 语言层面应该遵守什么规则。JMM 把内存抽象成两块主内存保存共享变量。工作内存每个线程自己的本地副本。线程之间不能直接访问对方的工作内存。线程 A 要把修改告诉线程 B必须通过主内存完成。不能直接通信主内存共享变量线程 A 工作内存变量副本线程 B 工作内存变量副本这就引出了可见性问题线程 A 修改了共享变量但线程 B 可能还在用自己的旧副本。volatile 解决什么volatile有两层核心语义保证线程间可见性。禁止特定指令重排序。先看可见性privatestaticvolatilebooleanstopfalse;publicstaticvoidmain(String[]args){newThread(()-{while(!stop){// busy loop}System.out.println(stopped);},t1).start();newThread(()-{stoptrue;},t2).start();}如果stop不加volatile线程t1可能一直读不到t2写入的新值。JIT 编译器还可能把循环优化得更激进让结果更难预测。加了volatile后写线程对stop的修改会对读线程可见。读线程主内存 stop写线程读线程主内存 stop写线程volatile 写 stop truevolatile 读看到新值跳出循环但要注意volatile不保证复合操作的原子性。下面这样仍然不安全volatileintcount0;publicvoidincrement(){count;}count还是读、改、写三步。volatile能保证每次读写的可见性但不能把三步合成一个原子操作。volatile 怎么禁止重排序CPU 和编译器为了性能可能会调整指令顺序。单线程下只要最终结果一致就行但多线程下其他线程可能观察到中间状态。PPT 里用 jcstress 做了一个例子intx;inty;Actorpublicvoidactor1(){x1;y1;}Actorpublicvoidactor2(II_Resultr){r.r1y;r.r2x;}如果出现r1 1, r2 0就说明线程 2 看到了y 1却没看到x 1。这在直觉上很奇怪因为代码里x 1写在y 1前面。给关键变量加volatileJVM 会在 volatile 读写附近插入内存屏障限制重排序。阻止前面的普通写跑到 volatile 写后面阻止后面的普通读跑到 volatile 读前面x 1 普通写volatile 写 y 1写屏障volatile 读 y读屏障读 x使用技巧可以简单记写变量时让volatile变量尽量放在发布动作的最后。读变量时让volatile变量尽量放在读取动作的最前。这不是死规矩但有助于理解“用一个 volatile 变量作为状态发布点”的模式。CAS 是什么CAS全称 Compare And Swap比较并交换。它体现的是乐观锁思想先不加互斥锁假设竞争不严重。更新时比较一下共享变量现在的值是不是自己当初看到的旧值如果是就更新如果不是说明被别人改过那就重试。CAS 有三个核心值名称含义V当前内存值A旧的预期值B准备更新的新值只有当V A时才把值改成B。是否读取当前值 A计算新值 B当前内存值 V 是否等于 A更新为 B更新失败重新读取并自旋重试用伪代码表示就是while(true){intoldValuevalue;intnewValueoldValue1;if(compareAndSwap(oldValue,newValue)){break;}}CAS 底层通常依赖 CPU 原子指令Java 里很多并发工具都会用到比如 Atomic 类、AQS 等。CAS 和 synchronized 怎么选PPT 里用了一个很口语化但很好记的对比synchronized是悲观锁想的是“别人一定会来改我先锁住”。CAS 是乐观锁想的是“别人不一定来改就算改了我再重试”。对比点synchronized / LockCAS思想悲观锁乐观锁线程状态竞争失败可能阻塞竞争失败通常自旋重试适合场景临界区较大、竞争激烈、逻辑复杂临界区很小、冲突不高风险阻塞和唤醒有成本高竞争下自旋浪费 CPU所以不要把 CAS 神化。竞争很低时 CAS 很漂亮竞争很高时大量线程一直自旋也会把 CPU 打满。面试怎么答可以这么组织并发程序出问题的根本原因主要是原子性、可见性、有序性。原子性指一组操作不能被中途打断比如i不是原子操作可以用synchronized、Lock或 Atomic 类解决。可见性指一个线程对共享变量的修改另一个线程能否及时看到volatile、锁都能保证可见性。有序性指编译器和 CPU 可能重排指令多线程下可能观察到异常结果volatile可以通过内存屏障限制重排序。JMM 定义了多线程读写共享变量的规则线程有自己的工作内存线程间通信必须经过主内存。CAS 是比较并交换是一种乐观锁思想。它比较当前内存值和旧预期值如果一致就更新否则自旋重试。Atomic 类和 AQS 都大量使用 CAS。CAS 适合冲突较少、操作很短的场景竞争激烈时自旋重试也会带来性能问题。
JMM、volatile 与 CAS:并发安全三大问题
发布时间:2026/6/4 3:58:02
并发程序为什么会出问题很多人会先说“因为多线程同时执行”。这句话没错但太粗了。真正落到 Java 面试里通常要拆成三个词原子性、可见性、有序性。synchronized、Lock、volatile、CAS、Atomic 类本质上都是围绕这三个问题在做不同取舍。并发安全到底在防什么PPT 里把 Java 并发编程三大特性列得很清楚特性问题常见解决方式原子性一组操作执行到一半被别的线程插进来synchronized、Lock、CAS、Atomic 类可见性一个线程改了共享变量另一个线程看不到volatile、synchronized、Lock有序性编译器或 CPU 为优化执行顺序导致多线程结果异常volatile、锁、happens-before 规则并发程序出问题原子性可见性有序性共享变量操作被打断线程本地缓存没有及时同步指令重排改变多线程观察结果synchronized / Lock / CASvolatile / synchronized / Lockvolatile 内存屏障 / 锁语义原子性ticketNum-- 不是一步下面这种扣库存逻辑单线程下没问题多线程下就危险intticketNum10;publicvoidgetTicket(){if(ticketNum0){return;}System.out.println(Thread.currentThread().getName() 抢到一张票, 剩余:ticketNum);ticketNum--;}ticketNum--看起来是一行代码实际不是一个不可分割的动作。它至少包含读取ticketNum。计算ticketNum - 1。写回ticketNum。两个线程可能同时读到1然后都扣减成功。主内存 ticketNum线程 T2线程 T1主内存 ticketNum线程 T2线程 T1读取 ticketNum 1读取 ticketNum 1写回 0写回 0解决原子性最直接的方式是加锁publicsynchronizedvoidgetTicket(){if(ticketNum0){return;}ticketNum--;}也可以用Lock或者在适合的场景下用 Atomic 类底层的 CAS。JMM 是什么JMM全称 Java Memory ModelJava 内存模型。它不是 JVM 内存结构里的堆、栈、方法区那套东西。JMM 讨论的是多线程读写共享变量时Java 语言层面应该遵守什么规则。JMM 把内存抽象成两块主内存保存共享变量。工作内存每个线程自己的本地副本。线程之间不能直接访问对方的工作内存。线程 A 要把修改告诉线程 B必须通过主内存完成。不能直接通信主内存共享变量线程 A 工作内存变量副本线程 B 工作内存变量副本这就引出了可见性问题线程 A 修改了共享变量但线程 B 可能还在用自己的旧副本。volatile 解决什么volatile有两层核心语义保证线程间可见性。禁止特定指令重排序。先看可见性privatestaticvolatilebooleanstopfalse;publicstaticvoidmain(String[]args){newThread(()-{while(!stop){// busy loop}System.out.println(stopped);},t1).start();newThread(()-{stoptrue;},t2).start();}如果stop不加volatile线程t1可能一直读不到t2写入的新值。JIT 编译器还可能把循环优化得更激进让结果更难预测。加了volatile后写线程对stop的修改会对读线程可见。读线程主内存 stop写线程读线程主内存 stop写线程volatile 写 stop truevolatile 读看到新值跳出循环但要注意volatile不保证复合操作的原子性。下面这样仍然不安全volatileintcount0;publicvoidincrement(){count;}count还是读、改、写三步。volatile能保证每次读写的可见性但不能把三步合成一个原子操作。volatile 怎么禁止重排序CPU 和编译器为了性能可能会调整指令顺序。单线程下只要最终结果一致就行但多线程下其他线程可能观察到中间状态。PPT 里用 jcstress 做了一个例子intx;inty;Actorpublicvoidactor1(){x1;y1;}Actorpublicvoidactor2(II_Resultr){r.r1y;r.r2x;}如果出现r1 1, r2 0就说明线程 2 看到了y 1却没看到x 1。这在直觉上很奇怪因为代码里x 1写在y 1前面。给关键变量加volatileJVM 会在 volatile 读写附近插入内存屏障限制重排序。阻止前面的普通写跑到 volatile 写后面阻止后面的普通读跑到 volatile 读前面x 1 普通写volatile 写 y 1写屏障volatile 读 y读屏障读 x使用技巧可以简单记写变量时让volatile变量尽量放在发布动作的最后。读变量时让volatile变量尽量放在读取动作的最前。这不是死规矩但有助于理解“用一个 volatile 变量作为状态发布点”的模式。CAS 是什么CAS全称 Compare And Swap比较并交换。它体现的是乐观锁思想先不加互斥锁假设竞争不严重。更新时比较一下共享变量现在的值是不是自己当初看到的旧值如果是就更新如果不是说明被别人改过那就重试。CAS 有三个核心值名称含义V当前内存值A旧的预期值B准备更新的新值只有当V A时才把值改成B。是否读取当前值 A计算新值 B当前内存值 V 是否等于 A更新为 B更新失败重新读取并自旋重试用伪代码表示就是while(true){intoldValuevalue;intnewValueoldValue1;if(compareAndSwap(oldValue,newValue)){break;}}CAS 底层通常依赖 CPU 原子指令Java 里很多并发工具都会用到比如 Atomic 类、AQS 等。CAS 和 synchronized 怎么选PPT 里用了一个很口语化但很好记的对比synchronized是悲观锁想的是“别人一定会来改我先锁住”。CAS 是乐观锁想的是“别人不一定来改就算改了我再重试”。对比点synchronized / LockCAS思想悲观锁乐观锁线程状态竞争失败可能阻塞竞争失败通常自旋重试适合场景临界区较大、竞争激烈、逻辑复杂临界区很小、冲突不高风险阻塞和唤醒有成本高竞争下自旋浪费 CPU所以不要把 CAS 神化。竞争很低时 CAS 很漂亮竞争很高时大量线程一直自旋也会把 CPU 打满。面试怎么答可以这么组织并发程序出问题的根本原因主要是原子性、可见性、有序性。原子性指一组操作不能被中途打断比如i不是原子操作可以用synchronized、Lock或 Atomic 类解决。可见性指一个线程对共享变量的修改另一个线程能否及时看到volatile、锁都能保证可见性。有序性指编译器和 CPU 可能重排指令多线程下可能观察到异常结果volatile可以通过内存屏障限制重排序。JMM 定义了多线程读写共享变量的规则线程有自己的工作内存线程间通信必须经过主内存。CAS 是比较并交换是一种乐观锁思想。它比较当前内存值和旧预期值如果一致就更新否则自旋重试。Atomic 类和 AQS 都大量使用 CAS。CAS 适合冲突较少、操作很短的场景竞争激烈时自旋重试也会带来性能问题。