济顾桥松在多核处理器成为主流的今天并发编程已成为每个Java程序员的必备技能。然而编写正确的并发程序远比单线程程序复杂主要原因在于我们需要处理两个核心问题线程之间如何通信线程之间如何同步Java内存模型JMM正是为了解决这些问题而设计的抽象概念。理解JMM不仅有助于编写正确的并发程序还能帮助我们更好地利用现代硬件的性能优势。2. Java内存模型的基础概念2.1 并发编程的两个关键问题通信机制共享内存 vs 消息传递/*** 共享内存模型示例* Java采用共享内存模型线程通过读写共享变量进行隐式通信*/public class SharedMemoryExample {private int sharedData 0; // 共享变量// 线程A通过写入共享变量与线程B通信public void threadA() {sharedData 42; // 隐式通信通过修改共享变量}// 线程B通过读取共享变量接收通信public void threadB() {if (sharedData 42) {System.out.println(收到线程A的消息);}}}现实比喻把共享内存想象成公司的公告板员工A在公告板上贴通知写共享变量员工B查看公告板获取信息读共享变量不需要直接对话通过公告板间接通信同步机制显式 vs 隐式/*** Java需要显式同步* 程序员必须明确指定哪些代码需要互斥执行*/public class ExplicitSynchronization {private final Object lock new Object();private int counter 0;public void increment() {synchronized(lock) { // 显式同步counter; // 临界区代码}}}2.2 JMM的抽象结构三层存储架构┌─────────────┐ ┌─────────────┐│ 线程A │ │ 线程B ││ │ │ ││ 本地内存A │ │ 本地内存B ││ ┌─────────┐ │ │ ┌─────────┐ ││ │共享变量 │ │ │ │共享变量 │ ││ │ 副本 │ │ │ │ 副本 │ ││ └─────────┘ │ │ └─────────┘ │└──────┬──────┘ └──────┬──────┘│ │└──────────────────┘│JMM控制交互│┌──────┴──────┐│ 主内存 ││ ┌─────────┐ ││ │共享变量 │ ││ └─────────┘ │└─────────────┘关键理解主内存存储所有共享变量的中央仓库本地内存每个线程的工作缓存包含共享变量的副本JMM控制主内存与本地内存之间交互的交通管理系统线程通信的详细过程public class ThreadCommunication {private int sharedVariable 0;public void demonstrateCommunication() {// 初始状态主内存和所有本地内存中 sharedVariable 0// 线程1执行sharedVariable 100; // 1. 修改本地内存中的副本// 2. 在某个时刻刷新到主内存// 线程2执行int value sharedVariable; // 3. 从主内存加载最新值// 4. 存入本地内存副本}}3. 重排序看不见的性能优化3.1 什么是重排序重排序是编译器和处理器为了优化程序性能而对指令序列进行重新排序的一种手段。public class ReorderingDemo {private int a 0, b 0;public void noReorder() {// 程序员看到的顺序a 1; // 操作1b 2; // 操作2int c a b; // 操作3}// 实际可能执行的顺序public void actualExecution() {b 2; // 操作2先执行a 1; // 操作1后执行重排序int c a b; // 操作3结果仍然是3}}现实比喻聪明的厨师优化做菜流程新手厨师严格按菜谱顺序先烧水→再切菜→最后煮面耗时8分钟资深厨师先烧水→在等水开时切菜→水开了煮面耗时5分钟结果相同3.2 数据依赖性重排序的底线三种数据依赖类型public class DataDependency {// 1. 写后读 (Write After Read)public void writeAfterRead() {a 1; // 写操作b a; // 读操作 ← 不能重排序}// 2. 写后写 (Write After Write)public void writeAfterWrite() {a 1; // 第一次写a 2; // 第二次写 ← 不能重排序}// 3. 读后写 (Read After Write)public void readAfterWrite() {b a; // 读操作a 1; // 写操作 ← 不能重排序}}数据依赖的现实意义public class CookingDependencies {public void makeSandwich() {// 有依赖的操作不能重排序bread toast(); // 必须先烤面包sandwich putFilling(bread); // 才能放馅料// 无依赖的操作可以重排序prepareLettuce(); // 准备生菜prepareTomato(); // 准备番茄 ← 顺序可以交换}}3.3 as-if-serial语义单线程的幻觉as-if-serial语义不管怎么重排序单线程程序的执行结果不能被改变。public class AsIfSerialExample {public double calculateArea() {double pi 3.14; // 操作Adouble r 1.0; // 操作Bdouble area pi * r * r; // 操作Creturn area; // 总是返回3.14无论A和B的执行顺序}}数据依赖分析A → Cpi用于计算areaB → Cr用于计算areaA ? BA和B没有依赖可以重排序3.4 重排序对多线程的影响问题代码示例public class ReorderingProblem {int a 0;boolean flag false;// 线程A执行public void writer() {a 1; // 操作1设置数据flag true; // 操作2发布标志}// 线程B执行public void reader() {if (flag) { // 操作3检查标志int i a * a; // 操作4使用数据System.out.println(结果: i);}}}两种危险的重排序情况1操作1和2重排序// 期望顺序a1 → flagtrue// 重排序后flagtrue → a1// 结果线程B可能看到flagtrue但a0情况2操作3和4重排序猜测执行// 期望顺序检查flag → 计算a*a// 重排序后提前计算a*a → 检查flag// 结果可能使用过期的a值进行计算4. 顺序一致性理想的内存模型4.1 什么是顺序一致性顺序一致性是一个理论参考模型为程序员提供极强的内存可见性保证。public class SequentialConsistency {// 两大特性// 1. 线程内顺序不变public void perfectOrder() {step1(); // 严格按顺序执行step2(); // 严格按顺序执行step3(); // 严格按顺序执行}// 2. 全局统一视图public void globalView() {// 所有线程看到相同的操作执行顺序// 操作立即对所有线程可见}}现实比喻完美的电影放映系统每个场景严格按剧本顺序播放所有观众看到完全相同的画面画面切换瞬间同步到所有观众4.2 顺序一致性的工作机制全局内存开关比喻[线程1] [线程2] [线程3] ... [线程N]↓ ↓ ↓ ↓┌─────────────────────────┐│ 全局内存开关 │ ← 像老式电话总机└─────────────────────────┘↓[全局内存]工作方式1. 开关每次只连接一个线程到内存2. 该线程执行一个完整操作3. 然后开关切换到下一个线程4. 所有操作串行执行全局可见4.3 JMM vs 顺序一致性public class JMMvsSequential {// 顺序一致性模型理想class IdealWorld {// - 单线程严格按程序顺序执行// - 所有线程看到相同的操作顺序// - 所有操作立即全局可见// - 所有操作原子执行}// JMM现实模型class RealWorld {// - 单线程内可能重排序优化// - 不同线程可能看到不同的操作顺序// - 写操作可能延迟可见本地缓存// - long/double可能非原子操作}}5. volatile的内存语义深度解析5.1 volatile的基本特性volatile与锁的等价性public class VolatileEquivalence {// 使用volatile的版本class VolatileVersion {volatile long counter 0;public void set(long value) {counter value; // 单个volatile写}public long get() {return counter; // 单个volatile读}}// 使用锁的等价版本class LockVersion {long counter 0;final Object lock new Object();public void set(long value) {synchronized(lock) {counter value;}}public long get() {synchronized(lock) {return counter;}}}}关键理解单个volatile变量的读写 ≈ 用同一个锁同步的普通变量读写但volatile ≠ 原子操作需要额外同步5.2 volatile的happens-before关系经典的volatile通信模式public class VolatileCommunication {private int data 0;private volatile boolean ready false; // volatile信号标志// 生产者线程public void producer() {data 42; // 1. 准备数据ready true; // 2. 发出信号volatile写}// 消费者线程public void consumer() {if (ready) { // 3. 检查信号volatile读int result data; // 4. 使用数据System.out.println(结果: result); // 保证输出42}}}happens-before关系链data 42 → ready true → if(ready) → result data↑ ↑ ↑ ↑步骤1 步骤2 步骤3 步骤4↓ ↓ ↓ ↓程序顺序规则 volatile规则 程序顺序规则 传递性规则关系推导1 happens-before 2程序顺序规则2 happens-before 3volatile规则写先于读3 happens-before 4程序顺序规则因此1 happens-before 4传递性结果如果消费者看到readytrue那么它一定能看到data425.3 volatile的内存语义volatile写发送消息public class MessageSending {// volatile写就像发送广播消息public void sendMessage() {prepareData(); // 准备消息内容messageReady true; // volatile写广播发送// 效果所有准备的数据连带消息一起发出}}volatile写的内存效果刷新线程本地内存中的所有共享变量到主内存确保写操作之前的所有修改都对其他线程可见volatile读接收消息public class MessageReceiving {// volatile读就像打开收件箱public void receiveMessage() {if (messageReady) { // volatile读检查新消息// 自动效果清空本地缓存重新加载所有数据processData(); // 处理接收到的数据}}}volatile读的内存效果使线程的本地内存无效强制从主内存重新加载所有共享变量5.4 volatile内存语义的实现内存屏障内存屏障的作用public class MemoryBarrierDemo {private int x, y;private volatile boolean flag;public void writer() {x 1; // 普通写y 2; // 普通写// StoreStore屏障确保x1, y2先完成flag true; // volatile写// StoreLoad屏障确保flagtrue立即可见}public void reader() {// LoadLoad屏障确保之前的读取完成if (flag) { // volatile读// LoadStore屏障确保后续写入基于正确状态int sum x y; // 保证看到x1, y2}}}四种内存屏障的详细解释屏障类型 作用 现实比喻 插入位置StoreStore 确保前面的写完成再执行后面的写 先炒完菜再装盘 volatile写之前StoreLoad 确保前面的写完成再执行后面的读 先生产完产品再质量检查 volatile写之后LoadLoad 确保前面的读完成再执行后面的读 先读完第一章再读第二章 volatile读之后LoadStore 确保前面的读完成再执行后面的写 先诊断病情再开药方 volatile读之后实际的屏障插入策略public class ActualBarrierInsertion {int a;volatile int v1 1;volatile int v2 2;void readAndWrite() {int i v1; // volatile读// LoadLoad屏障可能被省略int j v2; // volatile读// LoadStore屏障a i j; // 普通写// StoreStore屏障v1 i 1; // volatile写// StoreStore屏障v2 j * 2; // volatile写// StoreLoad屏障必须保留}}优化原理编译器根据具体上下文省略不必要的屏障但最后的StoreLoad屏障通常不能省略不同处理器平台有不同优化策略5.5 volatile的使用场景和限制适合使用volatile的场景public class GoodVolatileUse {// 场景1状态标志private volatile boolean shutdownRequested false;public void shutdown() {shutdownRequested true;}public void doWork() {while (!shutdownRequested) {// 正常工作}System.out.println(程序已停止);}// 场景2一次性安全发布private volatile Resource resource;public Resource getResource() {if (resource null) {synchronized(this) {if (resource null) {Resource temp new Resource();// volatile写确保对象完全构造后对其他线程可见resource temp;}}}return resource;}}不适合使用volatile的场景public class BadVolatileUse {// 错误volatile不能保证复合操作的原子性private volatile int counter 0;public void unsafeIncrement() {counter; // 这不是原子操作// 实际包含读 → 修改 → 写 三个步骤// 多线程环境下可能丢失更新}// 正确做法使用AtomicInteger或锁private AtomicInteger safeCounter new AtomicInteger(0);public void safeIncrement() {safeCounter.incrementAndGet(); // 原子操作}}6. 扩展知识MESI协议 - 硬件层面的缓存一致性6.1 MESI协议基础图书馆管理系统基础概念映射现实世界比喻大型企业图书馆系统─────────────────────────────────────技术概念 ? 现实比喻─────────────────────────────────────CPU核心 ? 不同部门的会议室缓存 ? 会议室里的白板主内存 ? 中央档案室缓存行 ? 白板上的一个主题区域MESI状态 ? 白板的使用权限状态总线 ? 公司内部广播系统内存屏障 ? 强制同步会议四种状态的现实意义public class MESIStateMetaphors {// Modified (M) - 已修改状态// 比喻你在会议室白板上做了独家修改还没同步到中央档案室// 特点只有你有最新版本别人看到的都是过时的// Exclusive (E) - 独占状态// 比喻你借了档案室的资料只有你的会议室有复印件// 特点你是唯一持有者可以随时修改// Shared (S) - 共享状态// 比喻多个会议室都有同一份资料的复印件// 特点大家看到的内容都一样但不能直接修改// Invalid (I) - 无效状态// 比喻你会议室的资料复印件已过期作废// 特点不能使用这份资料需要重新获取}6.2 MESI协议完整状态转换的比喻场景场景1第一次获取资料I → E技术场景CPU第一次读取未缓存的数据现实比喻市场部会议室初始状态I1. 需要年度销售报告资料2. 检查白板没有这份资料3. 通过广播系统谁有年度销售报告4. 其他部门都没回应说明没人有副本5. 中央档案室提供原始报告6. 市场部将报告贴到白板上标记独占(E)结果只有市场部有这份报告可以随时修改场景2共享阅读资料E → S / I → S技术场景第二个CPU读取同一数据现实比喻技术部会议室初始状态I1. 也需要年度销售报告2. 检查白板没有这份资料3. 广播我也需要年度销售报告市场部会议室状态E听到广播4. 回应我这里有可以共享5. 将自己白板标记改为共享(S)6. 提供复印件给技术部技术部会议室7. 获得复印件标记共享(S)结果两个部门都有相同报告都不能单独修改场景3修改共享资料S → M / S → I技术场景CPU写入共享数据现实比喻市场部会议室状态S1. 发现报告有错误需要修改2. 但不能直接修改因为是共享状态3. 广播我要修改报告请销毁你们的复印件技术部会议室状态S听到广播4. 立即销毁复印件5. 将白板标记改为无效(I)6. 回应已销毁市场部会议室7. 收到所有确认后修改报告8. 将标记改为已修改(M)结果只有市场部有最新版本技术部的副本已作废场景4读取已修改资料M → S / I → S技术场景其他CPU读取被修改的数据现实比喻技术部会议室状态I1. 需要查看最新报告2. 检查白板标记为I复印件已销毁3. 广播我需要最新的年度销售报告市场部会议室状态M听到广播4. 将修改后的报告复印一份送到中央档案室更新5. 提供最新复印件给技术部6. 将自己标记改为共享(S)技术部会议室7. 获得最新复印件标记共享(S)结果两个部门又都有相同的最新报告6.3 MESI协议与volatile的关系volatile如何利用MESI协议public class VolatileWithMESI {private volatile boolean flag false;private int data 0;public void writer() {data 42;// volatile写会触发// 1. 将缓存行状态改为M(Modified)// 2. 发送无效化消息给其他CPU// 3. 等待所有确认// 4. 将数据刷新到主内存flag true;}public void reader() {// volatile读会触发// 1. 检查缓存行状态如果是I(Invalid)// 2. 发送读请求到总线// 3. 从主内存或其他CPU获取最新数据if (flag) {// 由于MESI协议这里保证看到data42System.out.println(data);}}}MESI协议的消息类型比喻public class MESIMessageMetaphors {// 读请求 (Read)// 比喻谁有XXX资料借我看看// 目的获取资料的只读副本// 读无效化 (ReadInvalidate)// 比喻我要修改XXX资料请把你们的复印件都给我/销毁// 目的获取独占修改权// 无效化 (Invalidate)// 比喻你们手里的XXX资料已过期请立即销毁// 目的使其他副本失效// 写回 (WriteBack)// 比喻我把修改后的资料送回中央档案室更新// 目的将修改同步到主存储// 响应 (Response)// 比喻我这里有资料给你复印件// 目的提供数据给请求者}6.4 MESI协议的硬件实现细节CPU缓存结构public class CPUCacheStructure {// 典型的L1缓存结构class L1Cache {int size; // 32KBint associativity; // 8路组相联int lineSize; // 64字节CacheLine[] lines; // 缓存行数组// 每个缓存行包含class CacheLine {byte[] data; // 64字节数据int tag; // 地址标签MESIState state; // MESI状态boolean dirty; // 脏位int lruCounter; // LRU计数}}}总线工作机制比喻想象单车道大桥[处理器A] [处理器B] [处理器C] [处理器D]↓ ↓ ↓ ↓┌─────────────────────────────┐│ 交通警察 │ ← 总线仲裁器└─────────────────────────────┘↓[单车道大桥] ← 总线↓[对岸城市] ← 内存规则1. 每次只允许一辆车过桥总线事务2. 警察决定谁先过总线仲裁3. 过桥期间其他车等待4. 保证每辆车完整过桥原子性6.5 MESI协议的性能优化技术写缓冲区Write Buffer优化public class WriteBufferOptimization {// 问题CPU写操作需要等待总线响应造成停顿// 解决方案写缓冲器class WriteBuffer {Queue pendingWrites;public void queueWrite(WriteRequest req) {// 将写请求放入缓冲区pendingWrites.add(req);// CPU可以继续执行不必等待}}// 但这会引入内存重排序// load X; store Y; 可能被重排序为 store Y; load X;}失效队列Invalidate Queue优化public class InvalidateQueue {// 问题处理失效请求会阻塞CPU// 解决方案失效队列class InvalidateQueue {Queue pendingInvalidates;public void queueInvalidate(InvalidateRequest req) {// 快速确认失效将请求放入队列pendingInvalidates.add(req);sendAcknowledge(); // 立即发送确认}}// 风险CPU可能短暂看到过期的数据}6.6 MESI协议的现实意义伪共享问题public class FalseSharing {// 问题两个不相关的变量在同一缓存行class Problem {// 这两个变量可能在同一个64字节缓存行volatile long variableA; // CPU1频繁修改volatile long variableB; // CPU2频繁修改}// 结果// CPU1修改variableA → 缓存行状态M→S→M→S...// CPU2修改variableB → 缓存行状态M→S→M→S...// 大量不必要的缓存一致性流量// 解决方案缓存行对齐class Solution {volatile long variableA;long p1, p2, p3, p4, p5, p6, p7; // 填充到64字节volatile long variableB;}}MESI协议与Java内存模型的关系public class MESIAndJMM {// MESI协议提供了硬件基础// - 缓存一致性保证// - 内存操作的有序性基础// Java内存模型建立在MESI之上// - 通过内存屏障控制MESI状态转换// - 利用MESI协议实现volatile语义// - 在MESI基础上定义更高层次的抽象// 关系总结// MESI是物理实现JMM是编程接口// volatile是高级指令内存屏障是底层控制}7. 实战指南正确使用volatile7.1 volatile使用模式模式1状态标志public class StatusFlagPattern {private volatile boolean running true;public void start() {// 工作线程new Thread(() - {while (running) {// 执行工作任务doWork();}System.out.println(线程正常退出);}).start();}public void stop() {running false; // 其他线程可以立即看到这个变化}}模式2观察者模式public class ObserverPattern {private volatile int temperature;private volatile int humidity;// 传感器线程生产者public void updateReadings(int temp, int hum) {// 不需要同步因为volatile保证可见性temperature temp;humidity hum;}// 显示线程消费者public void display() {// 总是看到最新的一致数据System.out.printf(温度: %d, 湿度: %d%n, temperature, humidity);}}7.2 volatile与锁的选择指南场景 推荐方案 原因简单的状态标志 volatile boolean 轻量级性能好独立变量的可见性 volatile 避免锁的开销计数器 AtomicInteger 需要原子性复杂的数据结构 synchronized 需要互斥访问复杂的不变式 synchronized 需要原子性保证7.3 常见陷阱与解决方案public class CommonMistakes {// 陷阱1误以为volatile是原子的private volatile int count 0;public void wrongIncrement() {count; // ? 不是原子操作}public void correctIncrement() {synchronized(this) {count; // ? 使用锁保证原子性}}// 陷阱2多个volatile变量需要单独保护private volatile int x, y;public void wrongUpdate() {x 1; // ? 两个写操作之间可能被其他线程打断y 2;}public void correctUpdate() {synchronized(this) {x 1; // ? 使用锁保护复合操作y 2;}}}8. 总结与最佳实践8.1 核心要点总结JMM抽象结构主内存是共享变量的中央存储每个线程有本地内存作为工作缓存线程通过主内存进行间接通信重排序优化编译器和处理器为了性能会重排序指令数据依赖性操作不会被重排序单线程程序通过as-if-serial语义保证正确性顺序一致性理想的内存模型提供最强的保证实际JMM在正确同步时提供顺序一致性效果volatile关键字保证可见性和一定的有序性通过内存屏障实现内存语义适合状态标志、安全发布等场景MESI协议硬件层面的缓存一致性协议通过四种状态管理缓存行的权限volatile的内存语义在MESI协议基础上实现8.2 最佳实践建议public class BestPractices {// ? 推荐做法// 1. 使用volatile作为状态标志private volatile boolean initialized false;// 2. 一次性安全发布private volatile Singleton instance;// 3. 独立观察的变量private volatile int currentTemperature;// 4. 注意缓存行对齐避免伪共享private volatile long data1;private long padding1, padding2, padding3, padding4,padding5, padding6, padding7; // 填充private volatile long data2;// ? 避免做法// 1. 不要用volatile做计数器// private volatile int counter; // 错误// 2. 不要依赖多个volatile变量的复合操作// private volatile int x, y; // 需要额外同步// 3. 不要过度使用volatile在需要时才使用}8.3 最终思考理解Java内存模型、volatile关键字和底层的MESI协议是编写正确并发程序的基础。记住这些核心原则共享变量需要同步未同步的共享变量访问可能导致不可预测的结果volatile提供轻量级同步适合简单的可见性需求但不保证原子性正确同步的程序具有顺序一致性这是JMM给程序员的强保证MESI协议是硬件基础理解MESI有助于理解volatile和内存屏障的工作原理理解底层原理有助于调试当遇到奇怪的并发bug时理解JMM、内存屏障和MESI协议会很有帮助把整个系统想象成智能的交通管理系统共享变量 十字路口volatile 交通信号灯锁 交通警察内存屏障 交通规则MESI协议 车辆之间的通信协调系统