本文主要介绍该模型产生的问题的背景,要解决的问题,处理思路,相关的实现规则以及链接是互锁的。我希望读者阅读本文后可以对Java内存模型系统有一个相对清晰的了解。 ,知道了,知道为什么。
内存模型生成背景
在介绍Java内存模型之前,让我们首先了解物理计算机中的并发问题。了解这些问题可以澄清内存模型的背景。
物理机遇到的并发问题类似于虚拟机中的并发问题。物理机解决方案对于虚拟机的实现具有重要的参考意义。
物理机的并发问题
硬件效率
计算机处理器不可能处理仅由处理器“计算”就可以完成的大多数正在运行的任务。处理器至少需要与存储器进行交互,例如读取操作数据并存储操作结果。这种I / O操作非常难以消除(不可能仅通过寄存器来完成所有算术任务)。
由于计算机存储设备和处理器的计算速度相差几个数量级,为避免处理器等待慢速存储器完成读写操作,现代计算机系统增加了一层读取并将写入速度尽可能接近处理器Speed缓存。
高速缓存充当内存和处理器之间的缓冲区:将操作所需的数据复制到高速缓存中,以便该操作可以快速运行,然后在操作结束时从高速缓存同步回内存


缓存一致性问题
基于缓存的存储系统交互可以很好地解决处理器和内存速度之间的矛盾,但是由于它引入了一个新问题:缓存一致性,因此也给计算机系统带来了更高的复杂性。
在多处理器系统(或单处理器多核系统)中,每个处理器(每个核)都有自己的缓存,并且它们共享相同的主内存。
当多个处理器的计算任务都涉及相同的主内存区域时,可能会导致缓存数据不一致。
因此,每个处理器在访问高速缓存时都需要遵循某些协议,并且在读写时必须根据协议进行操作以保持高速缓存的一致性。


代码乱序执行优化问题
为了充分利用处理器内部的计算单元并提高计算效率,处理器可能会无序执行输入代码。
处理器将在计算后重新组织乱序执行的结果。无序优化可以确保执行结果与在单个线程中顺序执行的结果一致,但是不能保证程序中每个语句的计算顺序。输入代码中的顺序相同。


乱序执行技术是处理器进行的一种优化,它违反了代码的原始顺序,从而提高了计算速度。在单核时代,处理器保证了所做的优化不会导致执行结果与预期目标相去甚远,但是在多核环境中情况并非如此。
在多核环境中,如果一个核的计算任务取决于另一核计算任务的中间结果。
并且没有采取任何保护措施来读写相关数据,因此其顺序不能由代码的顺序来保证,并且处理器的最终结果可能与我们的逻辑所获得的结果完全不同。


以上图为例进行说明,CPU的core2中的逻辑B首先取决于core1中的逻辑A:
分析Java内存模型的组成
内存模型概念
为了更好地解决上述一系列问题,总结并提出了内存模型。我们可以将内存模型理解为在特定操作协议下对特定内存或缓存的读写访问过程的抽象。
不同体系结构的物理计算机可以具有不同的内存模型,而Java虚拟机也具有自己的内存模型。
Java虚拟机规范试图定义Java内存模型(Java Memory Model,JMM)来屏蔽各种硬件和操作系统的内存访问差异,以便可以在各种平台上实现Java程序。一致的内存访问效果,由于不同平台上物理计算机的内存模型不同,因此无需为每个平台定制开发程序。
更具体地说,Java内存模型的目标是定义程序中每个变量的访问规则,即在虚拟机中将变量存储在内存中以及从内存中删除变量的底层细节。
此处的变量(变量)与Java编程中提到的变量不同。它们包括实例字段,静态字段和组成数字对象的元素,但不包括局部变量和方法参数,因为后者是Thread私有的。

注意:如果局部变量是引用类型,则它所引用的对象可以由Java堆中的每个线程共享,但是引用本身位于Java堆栈的局部变量表中,该表是私有的。线程。
Java内存模型的组成
主存储器
Java内存模型规定所有变量都存储在主内存(Main Memory)中(此处的主内存与物理硬件的主内存具有相同的名称。两者可以相互比较,但这可以只是虚拟机内存(一部分)。
工作记忆
每个线程都有自己的工作内存(工作内存,也称为本地内存,可以类似于上述处理器缓存)。线程的工作内存将线程使用的变量存储在主内存中。共享变量的副本。
工作内存是JMM的抽象概念,实际上并不存在。它涵盖了缓存,写缓冲区,寄存器以及其他硬件和编译器优化。
Java内存模型的抽象图如下:


JVM内存操作的并发问题
结合上面介绍的物理机处理器的内存问题,可以类推地总结JVM内存操作问题。下面介绍的Java内存模型的执行和处理将着重解决这两个问题。
工作记忆数据的一致性
每个线程处理数据时,它将在使用的主内存中保存共享变量的副本。当多个线程的计算任务涉及相同的共享变量时,各个共享变量副本将不一致。如果发生这种情况,在这种情况下,将数据同步回主存储器时应优先使用谁的副本数据?
Java内存模型主要使用一系列数据同步协议和规则来确保数据一致性,这将在后面详细介绍。
指令重新排序优化
Java中的重新排序通常是对编译器或运行时环境采取的指令进行重新排序和执行以优化程序性能的一种方法。
重新排序分为两类:编译时重新排序和运行时重新排序,分别对应于编译时和运行时环境。
类似地,指令重排不是任意的重排,它需要满足以下两个条件:
在多线程环境中,如果线程处理逻辑之间存在依赖关系,则由于指令重新排序,执行结果可能与预期的不同。稍后,我将详细介绍如何解决Java内存模型中的这种情况。
Java内存之间的交互
在了解Java内存模型的一系列协议和特殊规则之前,我们首先了解Java中内存之间的交互操作。
交互式操作过程
为了更好地理解内存的交互操作,以线程通信为例,让我们看一下如何程之间同步值:


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

但是,如果多个线程同时将更新后的变量值刷新回主内存,则可能导致获得的值不是预期的结果。
例如:定义volatile int count = 0,两个线程同时执行count ++操作,每个线程执行500次,最终结果小于1000。
原因是每个线程都需要以下3个步骤来执行count ++:
在某个时刻,两个线程在步骤1中读取的值可能都是100,而在执行步骤2之后获得的值都是101.最后,将101的值刷新两次并保存到主存储器中。
禁止对指令进行重新排序,具体说明如下,禁止重新排序的规则如下:
普通变量仅确保在方法执行期间可以在所有依赖于赋值结果的位置上获得正确的结果,而不能保证赋值操作的顺序与程序代码中的执行顺序一致。
例如:
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内存屏障插入策略:
使用易失性变量的场景
总而言之,它是“写一次,到处读取”。一个线程负责更新变量,而其他线程仅读取变量(不更新变量),并根据变量的新值执行相应的逻辑。例如,状态标志将更新,观察者模型变量的值将被释放。
最终变量的特殊规则
我们知道最终成员变量必须在声明时初始化或在构造函数中初始化,否则将报告编译错误。
final关键字的可见性是指这样的事实,即在完成初始化之后,便在构造函数中声明了final修改后的字段,然后其他线程可以正确地看到final字段的值而无需同步。这是因为一旦完成初始化,最终变量的值就会立即写回到主存储器中。
同步的特殊规则
通过sync关键字包含的代码区域控制数据的读写:
长整型和双精度变量的特殊规则
Java内存模型要求锁定,解锁,读取,加载,分配,使用,存储和写入这八项操作是原子的。
但是,对于64位数据类型(长整型和双精度),模型中特别定义了相对宽松的规定:允许虚拟机划分未由volatile修改的64位数据的读取和写入操作分为两个32位操作。
也就是说,虚拟机可以选择不保证64位数据类型的加载,存储,读取和写入这四个操作的原子性。
由于这种非原子性,其他线程可能会读取尚未同步的“ 32位半变量”的值。
但是,在实际开发中,Java内存模型强烈建议虚拟机将64位数据的读写实现为原子级。
当前,各种平台下的商用虚拟机选择将64位数据读写操作视为原子操作。因此,在编写代码时,我们通常不需要特别声明将long和double变量用作volatile。
摘要
由于Java内存模型涉及一系列规则,因此Internet上的大多数文章都对这些规则进行了分析,但是许多文章并未解释为什么需要这些规则以及这些规则的作用。
实际上,这不利于初学者学习。绕开这些繁琐的规则很容易。我不知道为什么让我们谈谈我个人学习知识的经验:
学习知识的过程不仅等同于理解和记忆知识,而且还建立了知识所解决问题的输入和输出之间的联系。
知识的本质是解决问题,因此在学习之前,我们必须了解问题,理解问题的输入和输出,知识是从输入到输出的关系映射。
知识的学习应结合大量的实例来理解映射关系,然后对知识进行压缩。华罗庚说:“读一本书,再读一本书”,解释了这一原理,首先结合大量的例子来理解知识,然后再压缩知识。
以学习Java内存模型为例:
作者丨了解Java爱好者
本文来自电脑杂谈,转载请注明本文网址:
http://www.pc-fly.com/a/shoujiruanjian/article-367147-1.html
这种先人后己
你唱歌好听吗
还是澳门聪明些
不要开