前言
上周在做多线程业务汇总功能开发时,我心中产生了两个疑问:
- 多线程之间如何实现通信?也就是线程间依靠什么机制进行数据交换。
- 多线程之间如何实现同步?也就是如何管控不同线程间任务执行的先后顺序。
查资料后了解,线程间主流的通信机制分为两种:共享内存 与消息传递 ,而 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 若要实现通信,必须经过两个核心步骤:
- 线程 A 将自身本地内存中已更新的共享变量,刷新同步至主内存
- 线程 B 从主内存中,读取线程 A 已更新后的共享变量数据
⚠️注意
✔️线程之间无法直接访问彼此的本地内存 ,线程通信必须经由主内存中转 。
✔️JMM 正是通过规范主内存与各线程本地内存之间的数据交互规则,为 Java 程序提供内存可见性保障。
二、主内存与工作内存
1、交互协议实现流程

⚠️注意
✔️读入主内存变量 :
lock→read→load✔️线程内使用 / 修改 :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
✔️写流程:
assign→store→write✔️不会显式触发
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 原子操作并非同一概念,它只实现可见性和有序性,不会阻塞线程,也无法保证自增这类复合操作的原子性。