前言上周在做多线程业务汇总功能开发时我心中产生了两个疑问多线程之间如何实现通信也就是线程间依靠什么机制进行数据交换。多线程之间如何实现同步也就是如何管控不同线程间任务执行的先后顺序。查资料后了解线程间主流的通信机制分为两种共享内存与消息传递而 Java 采用的正是共享内存模型。一、Java内存模型Java 线程间的通信由Java 内存模型Java Memory ModelJMM统一控制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 → unlock2、八种原子操作主内存与线程本地内存间的数据交互具体如下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为什么禁止指令重排序如果不禁止就算能刷新内存如果允许指令重排序代码执行顺序乱了业务逻辑就直接出错了。a 1; volatile flag true;如果允许指令重排序CPU 可能擅自把顺序改成volatile flag true; // 先执行这行 a 1; // 后执行这行别的线程一看到flagtrue以为a1已经完成但实际上a还没赋值直接拿到脏数据逻辑错乱⚠️注意✔️刷新内存只能保证别人能看到最新值✔️禁止重排序是保证代码按你写的顺序执行不乱跳、不颠倒逻辑4、volatile和 synchronized在内存模型的区别4.1、synchronized会阻塞其他线程拿不到锁就阻塞排队有竞态、有上下文切换。⚠️举个例子✔️好比进房间办事一个人进去办 5 分钟外面所有人原地排队、挂机等待啥也干不了。4.2、volatile 硬件 lock 前缀硬件层面串行化只是一瞬间锁定总线 / 缓存行做内存同步不会阻塞其他线程所有线程依旧可以同时跑只是内存数据立刻可见、顺序不乱⚠️硬件 lockvolatile✔️锁的是内存总线一瞬间指令干完马上释放线程不阻塞、不挂起继续跑。✔️volatile 的 lock好比过独木桥每个人一秒快速冲过去桥瞬间被占但过完马上空出来后面人不用排队睡觉只是稍微等一下下继续走。五、面试题1、什么是Java 内存模型JMM面试里一旦被问到这个问题很多小伙伴很容易把Java 内存模型JMM和Java 内存结构搞混。一答题就跑偏到堆、虚拟机栈、GC 垃圾回收这些内容上最后答得和面试官真正想问的完全不是一回事。其实面试中问到 Java 内存模型根本不是考内存分区主要考察的都是多线程、Java 并发相关的知识点。2、volatile为什么不会重排序、不会被插队因为加了 locklock addl 前缀后硬件把这整条指令的所有内部微操作封装成一个不可分割的原子单元硬件层面串行化全程独占总线 / 缓存行做完才放行别的 CPU 只能等这一条指令硬件执行完但只是硬件内存层面短暂等待不是 Java 线程被挂起、阻塞、进队列六、总结JMM 定义了线程与主内存的数据交互规范依靠八大原子操作完成变量读写同步。synchronized 通过加锁、释放锁既能保证线程互斥又能刷新内存、保障可见性。volatile 借助内存屏障禁止指令重排序写变量强制刷新到主存读变量清空本地缓存底层硬件 lock 只是瞬时锁定总线做缓存同步和 JMM 的 lock 原子操作并非同一概念它只实现可见性和有序性不会阻塞线程也无法保证自增这类复合操作的原子性。