深入剖析Java内存模型与volatile关键字 📅 2026/6/26 3:14:55 计算机在运行程序时每条指令都是在CPU中执行的在执行过程中会涉及到数据的读写。我们知道程序运行的数据是存储在主存中这时就会有一个问题读写主存中的数据没有 CPU 中执行指令的速度快如果任何的交互都需要与主存打交道则会大大降低效率所以就有了 CPU寄存器、各级缓存、主存构成了一个速度与容量的平衡金字塔。有了 CPU 高速缓存虽然解决了效率问题但是它会带来一个新的问题数据一致性。缓存一致性问题的本质在程序运行中会将运行所需要的数据复制一份到 CPU 高速缓存中在进行运算时 CPU 不再和主存打交道而是直接从高速缓存中读写数据只有当运行结束后才会将数据刷新到主存中。举一个简单的例子i i 1;当线程运行这段代码时首先会从主存中读取 i 的值( 假设此时 i 1 )然后复制一份到 CPU 高速缓存中然后 CPU 执行 1 的操作此时 i 2然后将数据 i 2 写入到高速缓存中最后刷新到主存中。其实这样做在单线程中是没有问题的有问题的是在多线程中。如下假如有两个线程 A、B 都执行这个操作 i 两个线程从主存中读取 i 的值( 假设此时 i 1 )到各自的高速缓存中然后线程 A 执行 1 操作并将结果写入高速缓存中最后写入主存中此时主存 i 2 。线程B做同样的操作主存中的 i 仍然 2 。让我们通过一个时序图来理解问题所在线程A 线程B 主存 │ │ i1 ├─读取i───┐ │ │ │ │ │ │ │ 得到i1 │ │ │ │ │ │ │ │ 计算i12 │ │ │ │ ├─读取i─┐ │ │ 写入缓存 │ │ │ │ │ │ │ 得到i1 │ │ │ 刷新到主存 │ │ │ │ │ │ 计算i12 │ │ 主存i2←───────────────→│ │ │ │ │ 写入缓存 │ │ │ │ 刷新到主存 │ │ │ 主存i2←─→ │ │ │最终结果i2而非期望的3。这就是经典的缓存一致性问题。解决缓存一致性方案有两种通过在总线加 LOCK# 锁的方式通过缓存一致性协议MESI 协议解决方案实现机制优点缺点用场景总线加锁通过LOCK#信号锁住总线实现简单性能差串行化早期处理器MESI协议缓存行状态机管理性能好部分并行实现复杂现代处理器MESI协议核心状态Modified缓存行被修改与主存不一致Exclusive缓存行独占与主存一致Shared缓存行被多个CPU共享Invalid缓存行无效需重新加载方案分析第一种方案存在一个问题它是采用一种独占的方式来实现的即总线加 LOCK# 锁的话只能有一个 CPU 能够运行其他 CPU 都得阻塞效率较为低下。第二种方案缓存一致性协议MESI 协议它确保每个缓存中使用的共享变量的副本是一致的。Java内存模型JMM上面从操作系统层次阐述了如何保证数据一致性下面我们来看一下 Java 内存模型稍微研究一下它为我们提供了哪些保证以及在 Java 中提供了哪些方法和机制来让我们在进行多线程编程时能够保证程序执行的正确性。在Java中所有的实例、静态变量和数组元素都存储在堆内存中堆内存在线程之间是共享的。局部变量方法定义参数和异常数量参数是存放在Java虚拟机栈上面的。Java虚拟机栈是线程私有的因此不会在线程之间共享它们不存在内存可见性的问题也不受内存模型的影响。Java内存模型Java Memory Model 简称 JMM决定一个一个线程对共享变量的写入何时对其它线程可见。JMM定义了线程和主内存之间的抽象关系线程之间共享变量存储在主内存中每个线程都有一个私有的本地内存本地内存中存储了该线程共享变量的副本。本地内存是JMM的一个抽象概率并不真实的存在。它涵盖了缓存写缓存区寄存器以及其他的硬件和编译优化。Java内存模型的抽象概念图如下所示看完了Java内存模型的概念我们再来看看内存模型中主内存是如何和线程本地内存之间交互的。JMM定义了8个原子操作规范主内存与工作内存的交互┌───────────────────┐ ┌───────────────────┐ │ 主内存操作 │ │ 本地内存操作 │ ├───────────────────┤ ├───────────────────┤ │ 1. lock (锁定) │ │ 4. load (载入) │ │ 2. unlock (解锁) │←--→│ 5. use (使用) │ │ 3. read (读取) │ │ 6. assign (赋值) │ │ 8. write (写入) │ │ 7. store (存储) │ └───────────────────┘ └───────────────────┘主内存和本地内存间的交互主内存和本地内存的交互即一个变量是如何从主内存中拷贝到本地内存又是如何从本地内存中回写到主内存中的实现Java内存模型提供了8中操作来完成主内存和本地内存之间的交互。它们分别如下lock锁定作用于主内存的变量它把一个变量标识为一条线程独占的状态。unlock解锁作用于主内存的变量它把一个处于锁定状态的变量释放出来释放后的变量才能被其它线程锁定。read读取作用于主内存的变量它把一个变量从主内存传输到线程的本地内存中以便随后的load动作使用。load载入作用于本地内存的变量它把read操作从主内存中的到的变量值放入本地内存的变量副本中。use使用作用于本地内存的变量它把本地内存中一个变量的值传递给执行引擎每当虚拟机遇到一个需要使用到变量值的字节码指令时将会执行这个操作。assign赋值作用于本地内存的变量它把一个从执行引擎接收到的变量赋予给本地内存的变量每当虚拟机遇到一个给变量赋值的字节码指令时将会执行这个操作。store存储作用于本地内存的变量它把本地内存中的变量的值传递给主内存中以便后面的write操作使用。write写入作用于主内存的变量它把store操作从本地内存中得到的变量的值放入主内存的变量中。从上面8种操作中我们可以看出当一个变量从主内存复制到线程的本地内存中时需要顺序的执行read和load操作当一个变量从本地内存同步到主内存中时需要顺序的执行store和write操作。Java内存模型只要求上述的2组操作是顺序的执行的但并不要求连续执行。比如对主内存中的变量a 和 b 进行访问时有可能出现的顺序是read a read b load b load a。除此之外Java内存模型还规定了在执行上述8种基本操作时必须满足以下规则read/load 和 store/write 必须成对出现不允许一个线程丢弃它最近的assign操作。即变量在线程的本地内存中改变后必须同步到主内存中。不允许一个线程无原因的把数据从线程的本地内存同步到主内存中。不允许线程的本地内存中使用一个未被初始化的变量。一个变量在同一时刻只允许一个线程对其进行lock操作但是一个线程可以对一个变量进行多次的lock操作当线程对同一变量进行了多次lock操作后需要进行同样次数的unlock操作才能将变量释放。如果一个变量执行了lock操作则会清空本地内存中变量的拷贝当需要使用这个变量时需要重新执行read和load操作。如果一个变量没有执行lock操作那么就不能对这个变量执行unlock操作同样也不允许unlock一个被其它线程执行了lock操作的变量。也就是说lock 和unlock操作是成对出现的并且是在同一个线程中。对一个变量执行unlock操作之前必须将这个变量的值同步到主内存中去。内存屏障内存屏障Memory Barrier或有时叫做内存栅栏Memory Fence是一种CPU指令用于控制特定条件下的重排序和内存可见性问题。Java编译器也会根据内存屏障的规则禁止重排序。内存屏障可以被分为以下几种类型LoadLoad屏障对于这样的语句Load1; LoadLoad; Load2在Load2及后续读取操作要读取的数据被访问前保证Load1要读取的数据被读取完毕。StoreStore屏障对于这样的语句Store1; StoreStore; Store2在Store2及后续写入操作执行前保证Store1的写入操作对其它处理器可见。LoadStore屏障对于这样的语句Load1; LoadStore; Store2在Store2及后续写入操作被刷出前保证Load1要读取的数据被读取完毕。StoreLoad屏障对于这样的语句Store1; StoreLoad; Load2在Load2及后续所有读取操作执行前保证Store1的写入对所有处理器可见。它的开销是四种屏障中最大的。在大多数处理器的实现中这个屏障是个万能屏障兼具其它三种内存屏障的功能。volatilevolatile的三大特性