volatile关键字
- 解决可见性问题
- 解决指令重排序
- 不保证原子性
- volatile关键字底层原理——内存屏障
- 面试题
- volatile 关键字的作用是什么?
- volatile 和 synchronized 的区别?
- volatile 能替代锁吗?为什么?
- volatile 底层是怎么实现的?
- 什么是内存屏障?有哪些类型?
- DCL 单例为什么要加 volatile?
- volatile 变量写操作后,其他线程为什么能看到?
- 这段代码为什么会有问题?(count++)
- Java 内存模型(JMM)中的 8 个原子操作和 volatile 的关系?
- volatile 和 final 在内存语义上有什么区别?
- 除了 volatile,还有什么方式能保证可见性?
解决可见性问题
先看一段代码:
publicclassVisibilityDemo{privatestaticbooleanflag=false;publicstaticvoidmain(String[]args)throwsInterruptedException{//读线程newThread(()->{System.out.println("线程A:等待flag变为true");while(!flag){// 忙等待}System.out.println("线程A:检测到flag为true,退出");}).start();Thread.sleep(1000);//写线程newThread(()->{flag=true;System.out.println("线程B:已将flag设为true");}).start();}}结果分析:线程B将 flag 改为 true 后,线程A应该立即跳出循环。但实际运行结果往往是:线程A陷入了死循环,永远无法退出。
为什么会这样?这涉及到并发编程中第一个核心问题——可见性。
在 Java 内存模型(JMM)中,每个线程都有自己的工作内存(CPU 缓存),共享变量存储在主内存中。线程B修改了 flag,只是更新了自己工作内存中的副本,还没来得及同步到主内存;而线程A读取 flag 时,也只会从自己的工作内存中读取。两个线程各自维护着一份副本,自然看不到对方的变化。
volatile 的第一个作用就是解决这个问题:将 flag 声明为volatile boolean flag = false;后,任何线程对它的修改都会立即刷新到主内存,任何线程读取它时也会直接从主内存获取最新值,从而保证可见性。
解决指令重排序
解决了可见性问题,还有第二个陷阱。来看单例模式中的经典实现——双重检查锁定(Double-Checked Locking):
publicclassSingleton{privatestaticSingletoninstance;privateSingleton(){}publicstaticSingletongetInstance(){if(instance==null){// 第一次检查synchronized(Singleton.class){if(instance==null){// 第二次检查instance=newSingleton();}}}returninstance;}}这段代码看似完美,实际上存在隐患。问题出在instance = new Singleton()这一行,它不是原子操作,而是分为多个步骤:
- 分配内存空间
- 初始化对象(执行构造函数)
- 将 instance 引用指向分配的内存空间
为了提高执行效率,编译器和处理器可能会对指令进行重排序,将步骤3提前到步骤2之前。此时如果另一个线程恰好在 instance 不为 null 但对象还未初始化完成时调用getInstance(),就会拿到一个"半成品"对象,可能导致程序崩溃。
volatile 的第二个作用就是禁止指令重排序:将 instance 声明为private static volatile Singleton instance;后,JVM 会在 volatile 写操作前后插入内存屏障,确保instance = new Singleton()的执行顺序不会被重排,从而避免对象逸出问题。
不保证原子性
volatile 解决了可见性和有序性,但有一个重要的限制需要牢记:它不保证原子性。
publicclassAtomicityDemo{privatestaticvolatileintcount=0;publicstaticvoidmain(String[]args)throwsInterruptedException{CountDownLatchlatch=newCountDownLatch(10);for(inti=0;i<10;i++){newThread(()->{for(intj=0;j<1000;j++){count++;// 复合操作,非原子性}latch.countDown();}).start();}latch.await();System.out.println("期望值:10000,实际值:"+count);}}多次运行这段代码,你会发现 count 的值总是小于 10000。原因在于count++虽然看起来是一行代码,实际上包含三个步骤:读取值 → 加1 → 写回。volatile 只能保证每次读取时拿到最新的值,但无法防止多个线程交错执行这些步骤导致的丢失更新问题。
如果需要原子性操作,应该使用AtomicInteger或synchronized。
volatile关键字底层原理——内存屏障
volatile 的可见性和有序性,底层都是通过内存屏障(Memory Barrier)实现的。
内存屏障是一种 CPU 指令,分为四种类型:
| 屏障类型 | 作用 |
|---|---|
| LoadLoad | 读-读屏障,后面的读不能重排到前面的读之前 |
| StoreStore | 写-写屏障,后面的写不能重排到前面的写之前 |
| LoadStore | 读-写屏障,后面的写不能重排到前面的读之前 |
| StoreLoad | 写-读屏障,后面的读不能重排到前面的写之前(全能型,开销最大) (同时刷新写缓冲区) |
对于 volatile 变量的操作,JVM 会按以下策略插入内存屏障:
- volatile 写之前:插入 StoreStore 屏障,确保之前的普通写操作已完成
- volatile 写之后:插入 StoreLoad 屏障,强制将新值刷新到主内存
- volatile 读之后:插入 LoadLoad 和 LoadStore 屏障,禁止后续读写被重排到前面
这些屏障在底层会被优化。x86 本身就是强内存模型,它保证了
LoadLoad、LoadStore、StoreStore顺序,所以其实在 x86 上,volatile的底层实现通常只需要一个lock addl(对应StoreLoad)就足够了。
面试题
volatile 关键字的作用是什么?
- 保证可见性:一个线程修改 volatile 变量后,其他线程能立即看到最新值
- 禁止指令重排序:通过内存屏障确保有序性
- 不保证原子性:复合操作(如
count++)仍有并发风险
volatile 和 synchronized 的区别?
| 维度 | volatile | synchronized |
|---|---|---|
| 可见性 | ✅ | ✅ |
| 原子性 | ❌ | ✅ |
| 线程阻塞 | 不会 | 会 |
| 性能开销 | 低 | 高(有锁竞争) |
| 使用位置 | 变量修饰 | 方法/代码块 |
volatile 能替代锁吗?为什么?
- 不能,因为
volatile只保证可见性和有序性- 对于复合操作(读-改-写)无能为力,必须用锁或 CAS
- 举例:
count++用 volatile 依然会丢更新
volatile 底层是怎么实现的?
- JVM 层面:通过内存屏障(Memory Barrier)实现
- 对 volatile 变量的写操作,会插入StoreStore和StoreLoad屏障
- 对 volatile 变量的读操作,会插入LoadLoad和LoadStore屏障
- 硬件层面(x86):通过
lock前缀指令实现(如lock addl)lock会锁定总线或缓存行,强制将修改刷新到主存,并使其他 CPU 缓存行失效
什么是内存屏障?有哪些类型?
| 屏障类型 | 作用 |
|---|---|
| LoadLoad | 读-读屏障,后面的读不能重排到前面的读之前 |
| StoreStore | 写-写屏障,后面的写不能重排到前面的写之前 |
| LoadStore | 读-写屏障,后面的写不能重排到前面的读之前 |
| StoreLoad | 写-读屏障,后面的读不能重排到前面的写之前(全能型,开销最大) |
DCL 单例为什么要加 volatile?
instance = new Singleton()不是原子操作,分三步:
- 分配内存
- 初始化对象(执行构造方法)
- 将引用指向内存
- 步骤 2 和 3 可能被重排,导致对象"半初始化"时引用就不为 null
- 其他线程拿到这个"半成品"对象,可能出问题(字段为默认值)
- volatile 禁止了对象创建过程的重排序
volatile 变量写操作后,其他线程为什么能看到?
- JMM 规范:volatile 写会将当前工作内存的值强制刷新到主内存
- 同时会使其他 CPU 中该变量的缓存行失效(MESI 协议的 Invalidate 机制)
- 其他线程再读时,必须从主内存重新加载
- 这是 volatile 可见性的完整闭环
JMM
这段代码为什么会有问题?(count++)
volatileintcount=0;// 10个线程各加1000次count++;
count++是复合操作:读 → 加1 → 写- volatile 只保证读和写的可见性,但两次操作之间可能被其他线程打断
- 最终结果会小于 10000
- 解决方案:使用
AtomicInteger或synchronized
Java 内存模型(JMM)中的 8 个原子操作和 volatile 的关系?
- 8 个操作:lock、unlock、read、load、use、assign、store、write
- volatile 规定:对变量的 read-load-use 必须连续;assign-store-write 也必须连续
- 本质上是约束了这些原子操作的执行顺序,不允许中间插入其他操作
volatile 和 final 在内存语义上有什么区别?
- final 用于保证初始化安全性:构造函数执行完后,final 字段对任意线程可见
- volatile 保证的是每次读写的可见性
- 一个对象的所有 final 字段,在构造函数结束时会有一个"冻结"操作(StoreStore 屏障)
- final 的优势:初始化后不需要同步开销,适合不可变对象
final有点像string,但是string本质不是final实现
除了 volatile,还有什么方式能保证可见性?
synchronized:锁释放前会将工作内存刷新到主存Lock:与 synchronized 类似AtomicInteger等原子类:底层用 volatile 修饰 value 字段final:初始化安全性保证构造结束后的可见性Thread.join()/Thread.start():JMM 保证的 happens-before 规则