Java内存模型(JMM)用于阻止各种硬件和操作系统的内存访问差异,以便Java程序可以在各种操作系统平台下实现一致的访问效果。
Java内存模型指定所有变量都存储在主内存中,并且每个线程也都有其自己的工作内存。该变量的所有操作均在工作内存中执行,并且主存储器的数据无法读取和写入。
不同的线程无法直接访问对手工作内存中的变量,并且需要通过主内存完成线程之间的变量的传输。线程,工作内存和主内存之间的交互在下图中显示。
主内存和工作内存之间的交互式协议,即如何将变量从主内存复制到工作内存,以及如何将详细信息从工作存储器返回到主内存。
以下8个操作在Java内存模型中定义:
以下是线程A和B的修改过程中Initflag变量的一个示例(成功修改了最终修改):
如果变量从主内存复制到工作内存,并且按顺序执行读取加载。相反,该变量必须从工作内存回到主内存中同步,并且必须按顺序执行存储和写入。以上两个操作必须按顺序进行,但不必连续。他们可以插入它们之间的其他说明,例如变量A,B对主内存的访问,可以读取A读取b加载a load B load B.in加法,8个基本练习必须符合规则:
运行结果:运行此代码后,将没有预期的结果,您会发现每个运行程序的输出结果都不同。它们的数量少于200000。为什么?
该问题出现在自我提示操作“ Race ++”中。我们将使用Javap编译此代码以获取代码列表。发现只有一行代码的增加()方法由类文件中的4个字节代码指令组成。(返回指令不是由race ++生成,可以计算此指令,而无需计算),这很容易为了分析汇编失败的原因:当Getstatic指令将比赛的价值带到操作堆栈的顶部时,关键的波动性是关键的单词,可确保此时种族的价值正确,但是当执行时ICONST_1和IADD指令,其他线程可能增加了种族的价值,并且操作堆栈的价值成为过期的数据。因此,在推杆指令可能将较小的种族值同步到主内存之后,BYTE代码是字节代码挥发性:我们还可以分析参数输出反向汇编的分析。
由于挥发性变量只能确保可见性,因此,如果符合以下两个规则:
如果我们不符合以下两个规则,我们仍然必须通过锁定(同步或java.util.concurrent中的原子类)来确保原子。
以下是一个正确的示例。只要我们修改启动变量,我们就可以输入逻辑执行:
使用挥发性变量的第二种语义是禁止指令进行分类和优化。普通变量只能确保该方法中依赖分配结果的所有位置都可以获得正确的结果。与程序代码中的执行顺序相吻合。因为在执行线程期间无法感知到这一点,这是所谓的Java存储器模型中描述的“带有线程内部的序列化学”。
以下代码是标准的DCL单打代码,可以观察通过添加挥发性和不连接挥发性关键字生成的汇编代码之间的差异。
编译后,此代码是实例变量分配的一部分,如下所示:
发现关键更改是通过挥发性修改的变量。在分配(MOV%EAX,0x150(%ESI)的前面是分配操作)之后,执行了更多“锁定addl$0x0,(%ESP)”操作。此操作等同于内存屏幕(内存屏障或内存围栏),这意味着在排序时无法将以下指令分类到内存屏障之前的位置)。只有一个CPU访问存储器不需要内存屏障;一个或多个CPU访问相同的内存,并且观察到其中一个,另一个CPU需要内存屏障才能确保一致性。此指令(添加ESP寄存器0的值)显然是一个空操作(使用此空操作而不是空气操作指令NOPNOP指令),关键是锁定前缀,查询IA32手册,其作用是使其作用是使此CPU的缓存写入内存中。写作动作还将导致其他CPU或其他内核无效。此操作等于Cache中的变量中提到的“商店和写入”操作。其他CPU。
那么,为什么它说它被禁止进行分类说明呢?从硬件体系结构方面,指令重量表示CPU使用要开发并发送到相应电路单元的津贴进行处理,以处理未经单独的说明,该程序。但是,并不是说这些说明是任意重新安排的。CPU需要能够正确处理指令,以确保该过程能够获得正确的执行结果。例如,指令1地址A中的值添加了10,指令2的值2乘以值地址a和指令3中值B中的值降低到3。目前,指令1和指令2取决于订单顺序。无法重复排出 - (a+10)2和A2+10显然是不同的,但是指令3可以重新安排到指令1、2或中间,只要保证CPU可以不时获得正确的A和B值。因此,在此CPU中,沉重的排序仍然看起来有序。因此,当lockaddl $0x0(%ESP)指令将修改与内存同步时,这意味着所有以前的操作都已执行,因此“指令重分类不能跨越记忆障碍”的效果”。
解决挥发性的语义问题,让我们看一下在许多保证并发安全性中使用挥发性的重要性 - 它可以比使用其他同步工具更快地使我们的代码更快?锁(使用同步关键字或java.util.concurrent中的锁定),但是由于消除了虚拟机并优化了许多锁定锁的消除和优化,因此我们很难量化挥发性的速度比同步更快。如果挥发性与您自己进行比较,可以确定,挥发性变量读取操作的性能消耗几乎与普通变量没有什么不同,但是写作操作可能会较慢,因为它需要在本地代码中插入许多内存屏障说明。为了确保未在混乱中执行处理器。n锁。我们选择挥发性和锁定的唯一基础只是可挥发性的语义,以满足使用情况的需求。
Java内存模型需要锁定,解锁,读取,加载,分配,使用,存储,写入,所有这些都是原子,但是对于64位数据类型(长和双),JVM中相对定义的相对定义规格定义了一个相对法规:允许将其分为两个32位操作的读写操作,这些操作不是从挥发性的64位数据中得出的,也就是说,允许虚拟机器实施它不能保证64--位数据类型加载,存储,阅读,阅读,阅读,阅读,阅读,阅读,阅读,阅读,阅读,阅读,阅读,阅读,阅读,阅读,阅读,阅读,阅读,阅读,阅读,阅读,阅读,阅读,阅读,阅读,阅读,阅读,阅读,阅读,阅读,阅读,阅读,阅读,阅读,阅读,阅读,阅读,阅读,读取4个操作的原子性,其中有4个操作是长长和双重的被称为的非原子协议。
如果有多个线程可以共享一个不声明挥发性长或双重类型的变量,并且同时读取和修改它们,那么某些线程可能会读取一个非原始的线程,也不是其他线程修改值表示“半变量”的值。
但是,这种“半变量”的情况非常罕见(它不会出现在当前的商业Java虚拟机中),因为Java内存模型允许虚拟机不得读取将长和双变量读取为原子操作,以作为原子操作。,允许虚拟机选择将这些操作作为原子操作实施,并且还可以实施“强烈推荐”的虚拟机。在实际开发中,几乎所有平台下的商用虚拟机器几乎所有商业虚拟机选择64 -bit的读取操作数据作为原子操作。因此,我们通常不需要在编写代码时使用长和双变量。
Java内存模型的三个主要特征:原子,可见性,有序秩序
一个或多个操作,所有操作都执行,并且在执行过程中不会被任何因素中断,或者未执行。- 没有任何原子保护措施的急剧操作不是原子。
如何确保原子质?
如何确保可见性?
也就是说,程序执行的顺序是按照代码的顺序实现的。
假设线程中的“ i = 1”操作首先发生在线程B“ j = i”的操作中,则可以确定在线程B的操作后,变量j的值必须等于1。二:
现在再次考虑线程C,我们仍然保持线程A和线程B之间的关系,线程C在线程A和线程B操作之间出现,但是线程C与线程B没有关系,那么J'SWHAT是值?答案?是不确定的!1和2都是可能的,因为螺纹C对变量I的影响可能会被线B观察到,或者可能不会观察到。目前,线程B有阅读过期数据的风险,并且没有多线程安全性。
以下是Java存储器模型下的一些“自然”第一个关系关系。这些高级关系不需要任何同步辅助,并且可以直接在编码中使用。如果两个操作之间的关系不在本列中,也不能从以下规则中得出,则它们将没有顺序保证。虚拟机可以随意重新分配:
没有任何同步方法而无需任何同步手段的任何同步方法,可以建立的第一个规则。作者演示了如何使用这些规则来确定操作室是否是顺序的。对于阅读,写作和共享变量的操作,读者还可以在以下示例中感受到“时间顺序”和“首先发生”之间的区别:
以上显示了一组普通的Getter/Setter方法。假设存在螺纹A和B,则首先将A thread A(时间为time)调用“ setValue(1)”,然后threadB调用相同对象“ getValue()”,螺纹B接收到的返回值是多少?
让我们以提前原则来分析各种规则。由于这两种方法是由线程A和线程B调用的,而不是在线程中调用,因此编程规则不适用。由于没有同步块,因此自然不会自然发生。锁定和解锁是操作的,因此管道锁定规则不适用;由于值变量未通过挥发性关键字修改,因此不适用挥发性变量规则;后一个线程启动,终止,中断规则和对象最终规则与此处无关。因为没有适用的第一行规则,最后一个传输无法谈论它。因此,我们可以确定,尽管螺纹a在操作时间中是螺纹B之前的不螺纹-Safe。
那么如何修复此问题?我们至少有两个更简单的解决方案可供选择:将Getter/Setter方法定义为同步方法,以便可以应用管道锁定规则;或该值定义为挥发性变量,因为setter方法dioValue的原始值符合挥发性关键字的使用,因此您可以应用挥发性变量规则来实现高级关系。
在上面的示例中,我们可以得出结论:“第一次发生时间”的操作并不意味着此操作将是“第一个-in -in”。第一次发生了什么?不幸的是,该推论也未建立。一个典型的示例是多次提到的“指令重订单”。演示的示例如下:以下代码:
上述代码的两个分配语句是同一线程之一。根据程序的顺序规则,首先在“ int j = 2”中出现“ int i = 1”的操作,但可以完全证明“ int j = 2”的代码。处理器的执行确实不影响进步原则的正确性,因为我们不能在此线程中感知到这一点。当我们衡量并发安全问题时,不会被时间顺序中断