该文章首次发表在51CTO技术栈中
作者陈才华
一、内存模型生成背景
在介绍Java内存模型之前,让我们首先了解物理计算机中的并发问题。了解这些问题可以阐明内存模型的背景。物理机遇到的并发问题类似于虚拟机中的并发问题。物理机解决方案对于虚拟机的实现具有重要的参考意义。
物理机的并发问题
计算机处理器不可能处理只能由处理器“计算”完成的大多数正在运行的任务。处理器至少需要与存储器进行交互,例如读取操作数据并存储操作结果。这种I / O操作非常难以消除(不可能仅通过寄存器来完成所有算术任务)。
由于计算机的存储设备和处理器的计算速度相差几个数量级,因此,为了避免处理器等待慢速的内存读写操作完成,现代计算机系统增加了一层读和写功能。写入速度应尽可能接近处理器的计算速度。高速缓存高速缓存充当内存和处理器之间的缓冲区:操作所需的数据被复制到高速缓存中,以便操作可以快速运行,并且当操作结束时,它会从高速缓存同步回到内存。
面试官问:什么是Java内存模型?
基于缓存的存储系统交互可以很好地解决处理器和内存速度之间的矛盾,但是由于它引入了一个新的问题:缓存一致性,因此也给计算机系统带来了更高的复杂性。
在多处理器系统(或单处理器多核系统)中,每个处理器(每个核)都有自己的缓存,并且它们共享相同的主内存。当多个处理器的计算任务都涉及相同的主内存区域时,可能会导致缓存数据不一致。因此,每个处理器在访问缓存时都需要遵循某些协议,并且在读写时必须按照协议进行操作,以保持缓存的一致性。
面试官问:什么是Java内存模型?
为了充分利用处理器内部的算术单元并提高计算效率,处理器可能会执行输入代码的乱序执行,并且处理器将重组乱序的结果计算后执行顺序,并优化乱序可以保证执行结果与单个线程中顺序执行的结果一致,但不能保证程序中每个语句的计算顺序是一致的与输入代码中的顺序一致。
面试官问:什么是Java内存模型?
乱序执行技术是处理器进行的一种优化,它违反了代码的原始顺序,从而提高了计算速度。在单核时代,处理器保证了所做的优化不会导致执行结果与预期目标相去甚远,但是在多核环境中情况并非如此。
在多核环境中,如果一个核的中间计算任务依赖于另一计算任务的中间结果,并且未采取保护措施来读写相关数据,则不能保证其顺序按照代码的顺序。 ,由处理器获得的最终结果与由我们的逻辑获得的结果可能会有很大的差异。
面试官问:什么是Java内存模型?
上图是一个说明示例:CPU的core2中的逻辑B取决于core1中首先执行的逻辑A
二、 Java内存模型组成分析内存模型概念
为了更好地解决上述一系列问题,总结并提出了内存模型。我们可以将内存模型理解为在特定操作协议下对特定内存或缓存的读写访问过程的抽象。
具有不同体系结构的物理计算机可以具有不同的内存模型,而Java虚拟机也具有其自己的内存模型。 Java虚拟机规范试图定义Java内存模型(Java Memory Model,JMM)来屏蔽各种硬件和操作系统的内存访问差异,以便Java程序可以在各种平台下实现一致的内存。由于不同平台上物理计算机的内存模型不同,因此访问效果不需要为每个平台定制开发程序。
更具体地说,Java内存模型的目标是定义程序中每个变量的访问规则,即在虚拟机中将变量存储在内存中以及从内存中删除变量的底层细节。 。这里的变量与Java编程中提到的变量不同。它们包括实例字段,静态字段和组成数字对象的元素,但不包括局部变量和方法参数,因为后者是线程专有的。 (如果局部变量是引用类型,则它所引用的对象可以由Java堆中的每个线程共享,但是引用本身位于Java堆栈的局部变量表中,该表对线程是私有的。)

Java内存模型的组成
Java内存模型的抽象图如下:
面试官问:什么是Java内存模型?
JVM内存操作的并发性
结合上面介绍的物理机处理器的内存问题,可以类推地总结JVM内存操作问题。下面介绍的Java内存模型的执行和处理将着重解决这两个问题:
在多线程环境中,如果线程处理逻辑之间存在依赖关系,则由于指令的重新排序,执行结果可能与预期的不同。稍后我们将扩展Java内存模型来解决这种情况。
三、 Java内存之间的交互
在了解Java内存模型的一系列协议和特殊规则之前,我们首先了解Java内存之间的交互。
交互式操作过程
为了更好地理解内存的交互操作,以线程通信为例,让我们看一下如何程之间同步值:
面试官问:什么是Java内存模型?
线程1和线程2在主内存中都有共享变量x的副本。最初,这三个内存中的x值为0。将线程1中的x值更新为1后,与线程2同步主要涉及两个步骤:
总体而言,这两个步骤是线程1向线程2发送消息,并且此通信过程必须经过主内存。线程对变量的所有操作(读取,分配)必须在工作内存中执行。不同的线程无法直接访问彼此工作内存中的变量。线程之间的变量值的传递需要通过主存储器完成,以便每个线程都可以提供共享变量的可见性。
记忆互动的基本操作
关于主存储器与工作存储器之间的特定交互协议,即如何将变量从主存储器复制到工作存储器,如何从工作存储器同步回主存储器以及其他实现细节,Java内存模型定义了以下八个导入操作来完成。
实施虚拟机时,必须确保以下描述的每个操作都是原子的且不可分割的(对于双变量和长变量,某些平台上允许装入,存储,读取和写入操作。稍后介绍。)
8个基本操作
面试官问:什么是Java内存模型?
四、 Java内存模型操作规则
4. 1基本的内存交互操作的三个特征
在介绍内存交互的特定8个基本操作之前,有必要介绍该操作的3个特征。 Java内存模型是围绕如何在并发过程中处理这三个特征而构建的。首先,首先简要介绍其定义和基本实现,然后逐步进行分析。
Java内存模型的一系列操作规则似乎有点麻烦,但是总而言之,它是围绕原子性,可见性和顺序性的特征构建的。归根结底,就是要实现多线程工作内存中共享变量的数据一致性,多线程并发,并且程序可以在针对指令重排序优化的环境中按预期运行。
4. 2事前发生关系
在介绍这一系列规则之前,先了解先发生后发生的关系:用于描述接下来两个操作的内存可见性:如果操作A发生在操作B之前,则A的结果对B是可见的。事前发生关系的分析需要分为单线程和多线程情况:
为了促进程序开发,Java内存模型实现了以下支持事前发生关系的操作:
4. 3记忆屏障
如何确保Java中基础操作的顺序和可见性?可以通过内存屏障。
内存屏障是在两个CPU指令之间插入的一条指令,用于防止对处理器指令进行重新排序(如屏障)以确保顺序性。另外,为了达到屏障的效果,还将导致处理器在写入或读取值之前将主存储器的值写入高速缓存,并清除无效队列以确保可见性。
例如:
Store1;
Store2;
Load1;
StoreLoad; //内存屏障
Store3;
Load2;
Load3;
对于上述一组CPU指令(存储代表写指令,负载代表读指令),StoreLoad屏障前的Store指令不能与StoreLoad屏障后的Load指令交换位置,即重新排序。但是,StoreLoad屏障前后的指令可以互换,即Store1和Store2可以互换,Load2和Load3可以互换。
共有4种常见障碍
在一般代码中不容易看到Java中使用内存屏障。通过volatile和sync关键字修改的通用代码块(稍后将在更多介绍中进行修改)也可以通过Unsafe类使用内存屏障。
4. 4 8种操作同步规则
JMM执行上述八个基本操作时,为了确保内存之间的数据一致性,JMM中必须满足以下规则:
这些规则似乎有些繁琐,但并不难理解:
4. 5易失性变量的特殊规则
volatile的中文含义不稳定且易变。使用volatile修改变量是为了确保变量的可见性。
volatile的语义
volatile主要具有以下两种语义
语义1保证可见性
确保不同线程对变量操作的内存可见性。
此处的保证可见性与可变变量的并发操作的安全性不同,并且是确保可见性的具体说明:
线程写易失性变量的过程:
线程读取易失性变量的过程:

但是,如果多个线程同时将更新后的变量值刷新回主内存,则该值可能不是预期的结果:
例如:定义volatile int count = 0,两个线程同时执行count ++操作,每个线程执行500次,最终结果小于1000。原因是每个线程执行count ++要求以下3个步骤:
禁止语义2命令重新排序
有关具体说明,禁止重新排序的规则如下:
普通变量仅确保在方法执行期间可以在所有依赖于赋值结果的位置上获得正确的结果,而不能保证赋值操作的顺序与程序代码中的执行顺序一致。
例如:
volatile boolean initialized = false;
// 下面代码线程A中执行
// 读取配置信息,当读取完成后将initialized设置为true以通知其他线程配置可用
doSomethingReadConfg();
initialized = true;
// 下面代码线程B中执行
// 等待initialized 为true,代表线程A已经把配置信息初始化完成
while (!initialized) {
sleep();
}
// 使用线程A初始化好的配置信息
doSomethingWithConfig();
如果在上面的代码中定义初始化变量时未使用volatile修改,则可能是由于指令重新排序的优化,线程A中的最后一个代码“ initialized = true”将在“ doSomethingReadConfg( )”,因此它将导致使用线程B中的配置信息的代码出错,并且禁止重新排序的volatile关键字的语义可以防止这种情况的发生。
易失性变量的实现原理
具体的实现方法是,在编译时生成字节码时,将在指令序列中添加内存屏障,以确保以下是基于保守策略的JMM内存屏障插入策略:
面试官问:什么是Java内存模型?
使用易失性变量的场景
总而言之,它是“写一次,到处读取”。一个线程负责更新变量,而其他线程仅读取变量(不更新变量),并根据变量的新值执行相应的逻辑。例如,状态标志将更新,观察者模型变量的值将被释放。
4. 6最终变量的特殊规则
我们知道最终成员变量必须在声明时初始化或在构造函数中初始化,否则将报告编译错误。 final关键字的可见性意味着在初始化完成后,可以在构造函数中声明final修改后的字段,一旦完成初始化,就可以在其他线程中正确看到final字段的值而无需同步。这是因为一旦完成初始化,最终变量的值就会立即写回到主存储器中。
4. 7同步的特殊规则
通过sync关键字包含的代码区域控制数据的读写:
4. 8长和双变量的特殊规则
Java内存模型要求锁定,解锁,读取,加载,分配,使用,存储和写入这8个操作是原子操作,但是对于64位数据类型(长整数和双精度),相对定义是专门的在模型中定义。宽松的规定:允许虚拟机将未被volatile修改的64位数据的读取和写入操作划分为两个32位操作。换句话说,虚拟机可以选择不保证64位数据类型的加载,存储,读取和写入这四个操作的原子性。由于这种非原子性,可能导致其他线程读取尚未同步的“ 32位半变量”的值。
但是,在实际开发中,Java内存模型强烈建议虚拟机实现原子地读写64位数据。当前,各种平台下的商用虚拟机选择使用64位数据读取和写入操作作为原子操作。因此,在编写代码时,我们通常不需要特别声明将long和double变量用作volatile。
五、摘要
由于Java内存模型涉及一系列规则,因此Internet上的大多数文章都对这些规则进行了分析,但是许多文章并未解释为什么需要这些规则。这些规则的作用实际上不利于初学者学习,并且很容易解决。我不知道为什么这些繁琐的规则如此。让我们谈谈我个人学习知识的经验:
学习知识的过程不仅等同于理解知识和记忆知识,而且还可以在知识所解决的问题的输入和输出之间建立联系。知识的本质是解决问题,因此您必须在学习之前理解和理解问题。所需的输出和输出,而知识是从输入到输出的关系映射。知识的学习需要大量示例来理解这种映射关系,然后压缩知识。华罗庚说:“读书要厚,再读书要薄”。这就是解释。首先,通过大量示例了解知识。然后压缩知识。
以学习Java内存模型为例:
本文来自电脑杂谈,转载请注明本文网址:
http://www.pc-fly.com/a/shoujiruanjian/article-367157-1.html
难喝要死
都是骗