面试总被问 Java内存模型和 volatile,为什么总答不到点子上?

前言

上周在做多线程业务汇总功能开发时,我心中产生了两个疑问:

  1. 多线程之间如何实现通信?也就是线程间依靠什么机制进行数据交换。
  2. 多线程之间如何实现同步?也就是如何管控不同线程间任务执行的先后顺序。

查资料后了解,线程间主流的通信机制分为两种:共享内存消息传递而 Java 采用的正是共享内存模型


一、Java内存模型

Java 线程间的通信由Java 内存模型(Java Memory Model,JMM) 统一控制,JMM 的核心作用是定义一个线程对共享变量的写入操作,何时对其他线程可见

1、实现原理

2、主内存和本地内存区别

JMM 规范了线程与主内存之间的抽象交互关系:

  • 所有线程共享的变量都会存储在主内存中;
  • 而每个线程都拥有独立的本地内存,用于存放共享变量的工作副本。

2.1、主内存(JMM 抽象)

  • 对应硬件:计算机物理内存(RAM 内存条) 为主
  • 可以近似理解:JMM 主内存 ≈ 硬件物理内存
  • 存放所有共享变量,是所有线程共享的区域。

2.2、本地内存(JMM 抽象)

完全不是一块独立的物理内存 ,它是JMM 虚构的抽象合集, 包含了硬件里这些东西:

  • CPU 高速缓存(L1、L2、L3 缓存)
  • CPU 寄存器
  • 硬件写缓冲区
  • 编译器指令重排序、CPU 乱序执行优化

⚠️需要注意

✔️本地内存是 JMM 的抽象概念,并非物理真实存在 ,它涵盖了 CPU 缓存、硬件写缓冲区、CPU 寄存器,以及各类硬件和编译器的优化机制。

✔️按照 JMM 的严格规定,线程对共享变量的所有读写操作,必须在自身的本地内存中完成,不能直接读写主内存

3、线程之间如何通信?

结合1中的图可以看出,线程 A 与线程 B 若要实现通信,必须经过两个核心步骤:

  1. 线程 A 将自身本地内存中已更新的共享变量,刷新同步至主内存
  2. 线程 B 从主内存中,读取线程 A 已更新后的共享变量数据

⚠️注意

✔️线程之间无法直接访问彼此的本地内存 ,线程通信必须经由主内存中转

✔️JMM 正是通过规范主内存与各线程本地内存之间的数据交互规则,为 Java 程序提供内存可见性保障


二、主内存与工作内存

1、交互协议实现流程

⚠️注意

✔️读入主内存变量lockreadload

✔️线程内使用 / 修改 :use → assign

✔️写回主内存:store → write → unlock

2、八种原子操作

主内存与线程本地内存间的数据交互,具体如下:

  • lock(锁定) :作用于主内存变量,将变量标记为当前线程独占状态。
  • unlock(解锁) :作用于主内存变量,释放已处于锁定状态的变量,释放后其他线程才可对其加锁。
  • read(读取) :作用于主内存变量,将变量值从主内存传输到线程工作内存,供后续 load 操作使用。
  • load(载入) :作用于工作内存变量,把 read 从主内存读取到的值,存入工作内存的变量副本中。
  • use(使用) :作用于工作内存变量,将工作内存中的变量值传递给虚拟机执行引擎;只要虚拟机遇到需要读取变量值的字节码指令,就会触发该操作。
  • assign(赋值) :作用于工作内存变量,把执行引擎运算后得到的值,赋值给工作内存中的变量;虚拟机遇到变量赋值类字节码指令时,便会执行此操作。
  • store(存储) :作用于工作内存变量,将工作内存的变量值传输到主内存,供后续 write 操作使用。
  • write(写入) :作用于主内存变量,把 store 从工作内存传出的值,最终写入更新到主内存的变量中。

三、 锁的可见性原理

1、锁的获取与释放

  • 获取锁时 :JMM 会将当前线程的本地内存置为无效,强制线程从主内存读取共享变量的最新值。
  • 释放锁时 :JMM 会将当前线程本地内存中修改过的共享变量,强制刷新回主内存

⚠️**Synchronized关键字依托这一内存原理,实现了多线程访问共享资源时的 互斥性与可见性**

✔️在获取锁前,线程会从主内存加载最新数据;

✔️释放锁时,线程会将修改后的数据同步回主内存,确保其他线程能看到最新值。


四、volatile可见性原理

1、volatile 读写

  • volatile 写 :当线程写入一个 volatile 变量时,JMM 会强制将该线程本地内存中的变量值,直接刷新到主内存。
  • volatile 读 :当线程读取一个 volatile 变量时,JMM 会将该线程对应的本地内存置为无效,强制线程从主内存中读取变量的最新值。

⚠️注意

✔️读流程:read → load → use

✔️写流程:assignstorewrite

✔️不会显式触发 lock/unlock 这两个操作。

2、volatile 不显式触发 lock/unlock怎么保持数据一致性呢?

为实现 volatile 写刷新主存、读清空本地缓存、禁止指令重排序 的内存语义,JVM 会在机器码中插入**内存屏障**。

在 x86 平台下,写入 volatile 变量时,会生成带有 lock 前缀的汇编指令,例如 lock addl

该硬件 lock 基于总线锁和缓存一致性协议实现和 JMM 的 lock 原子操作不是一回事

它有三个核心作用:

  • 强制将写缓冲区、CPU 缓存中的数据立刻刷新到主内存;
  • 禁止指令重排序;
  • 触发缓存一致性协议,使其他 CPU 缓存副本失效,保障可见性。

3、volatile为什么禁止指令重排序?

如果不禁止,就算能刷新内存,如果允许指令重排序,代码执行顺序乱了,业务逻辑就直接出错了。

ini 复制代码
a = 1; 
volatile flag = true;

如果允许指令重排序, CPU 可能擅自把顺序改成:

ini 复制代码
volatile flag = true;  // 先执行这行 
a = 1;                 // 后执行这行

别的线程一看到 flag=true,以为 a=1 已经完成,但实际上 a 还没赋值,直接拿到脏数据 逻辑错乱

⚠️注意

✔️刷新内存只能保证别人能看到最新值

✔️禁止重排序是保证代码按你写的顺序执行,不乱跳、不颠倒逻辑

4、volatile和 synchronized在内存模型的区别

4.1、synchronized

阻塞其他线程,拿不到锁就阻塞排队,有竞态、有上下文切换。

⚠️举个例子

✔️好比进房间办事:一个人进去办 5 分钟,外面所有人原地排队、挂机等待,啥也干不了。

4.2、volatile + 硬件 lock 前缀
硬件层面串行化 ,只是一瞬间锁定总线 / 缓存行做内存同步
不会阻塞其他线程,所有线程依旧可以同时跑,只是内存数据立刻可见、顺序不乱

⚠️硬件 lock(volatile)

✔️锁的是内存总线一瞬间 ,指令干完马上释放,线程不阻塞、不挂起,继续跑

✔️volatile 的 lock 好比过独木桥:每个人一秒快速冲过去,桥瞬间被占,但过完马上空出来,后面人不用排队睡觉,只是稍微等一下下继续走。


五、面试题

1、什么是Java 内存模型(JMM)?

面试里一旦被问到这个问题,很多小伙伴很容易把Java 内存模型(JMM)Java 内存结构搞混。一答题就跑偏到堆、虚拟机栈、GC 垃圾回收这些内容上,最后答得和面试官真正想问的完全不是一回事。

其实面试中问到 Java 内存模型,根本不是考内存分区,主要考察的都是多线程、Java 并发相关的知识点。

2、volatile为什么不会重排序、不会被插队?

因为加了 lock (lock addl ) 前缀后:

  • 硬件把这整条指令的所有内部微操作,封装成一个不可分割的原子单元
  • 硬件层面串行化,全程独占总线 / 缓存行,做完才放行
  • 别的 CPU 只能等这一条指令硬件执行完 ,但只是硬件内存层面短暂等待
  • 不是 Java 线程被挂起、阻塞、进队列

六、总结

JMM 定义了线程与主内存的数据交互规范,依靠八大原子操作完成变量读写同步。

synchronized 通过加锁、释放锁,既能保证线程互斥,又能刷新内存、保障可见性。

volatile 借助内存屏障禁止指令重排序,写变量强制刷新到主存,读变量清空本地缓存;底层硬件 lock 只是瞬时锁定总线做缓存同步,和 JMM 的 lock 原子操作并非同一概念,它只实现可见性和有序性,不会阻塞线程,也无法保证自增这类复合操作的原子性。

相关推荐
Solis42 分钟前
吊打 HashTable!ConcurrentHashMap 凭什么成为并发神器
后端
神奇小汤圆1 小时前
同事说Spring循环依赖很简单,直到我们线上炸了...
后端
XovH1 小时前
环境搭建与第一个“Hello, World”:Django 项目结构与 MTV 模式详解
后端
阿丰资源1 小时前
基于SpringBoot的电影评论网站(含源码)
java·spring boot·后端
小码哥0681 小时前
2026版基于springboot的家政服务预约系统
java·spring boot·后端
浪荡Ddddd1 小时前
初识SpringAI:chat篇
后端·程序员
小谢小哥1 小时前
59-消息推送系统详解
java·后端·架构
杨运交1 小时前
[016][web模块]基于 MDC 的分布式追踪框架设计与实现
spring boot·后端
panshihao1 小时前
SSE 是什么?从原理到实战(Java+Vue+Node全示例)
java·后端·http