Java内存模型指的是什么

技术Java内存模型指的是什么本篇内容介绍了“Java内存模型指的是什么”的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让小编带领大家学习一下如何处理这些情况吧!希望大家仔细阅读,能够学有所成!J

本文介绍了“Java内存模型是什么意思”的知识。很多人在实际案例的操作中会遇到这样的困难。接下来,让边肖带领大家学习如何应对这些情况!希望大家认真阅读,学点东西!

00-1010我们常说的JVM内存模式是指JVM的内存分区;然而,Java内存模型是虚拟机的规范。Java虚拟机的规范中确实没有定义Java内存模型(JMM),用来屏蔽各种硬件和操作系统之间的内存访问差异,从而达到各种平台上Java程序一致的并发效果。JMM规范了Java虚拟机和计算机内存如何协同工作:它指定了一个线程如何以及何时可以看到其他线程修改的共享变量的值,以及如何在必要时同步访问共享变量。

原来的Java内存模型存在一些缺点,所以在Java1.5中对Java内存模型进行了修改,这个版本的Java内存模型在Java8中仍然在使用。

Java内存模型(不仅仅是JVM内存分区):调用栈和局部变量存储在线程栈上,对象存储在堆上。

内容示意图Java内存模型指的是什么

硬件角度示意图Java内存模型指的是什么

局部变量可以是原语类型,在这种情况下,它总是“停留”在线程堆栈上。

局部变量也可以是对对象的引用。在这种情况下,引用(这个局部变量)存储在线程堆栈上,但是对象本身存储在堆上。

对象可能包含方法,方法可能包含局部变量。这些局部变量仍然存储在线程堆栈中,即使这些方法所属的对象存储在堆中。

对象的成员变量可以与对象本身一起存储在堆中。此成员变量是基元类型还是引用类型。

静态成员变量也与类定义一起存储在堆中。

存储在堆上的对象可以被所有引用该对象的线程访问。当一个线程可以访问一个对象时,它也可以访问该对象的成员变量。如果两个线程同时在同一个对象上调用同一个方法,它们都将访问这个对象的成员变量,但是每个线程都有这个成员变量的私有副本。

00-1010现代硬件内存模型不同于Java内存模型,因此了解内存模型的体系结构以及Java内存模型如何与之协同工作非常重要。

现代计算机硬件架构简图:Java内存模型指的是什么

多CPU:现代计算机通常由两个或多个CPU组成。其中一些处理器有多核。由此可见,在一台拥有两个或更多CPU的现代计算机上,同时运行多个线程是可能的。每个CPU一次运行一个线程没有问题。这意味着,如果您的Java程序是多线程的,那么您的Java程序中每个CPU上的一个线程可能会同时(并发)执行。

中央处理器寄存器:每个中央处理器包含一系列寄存器,这些寄存器是中央处理器内存的基础。CPU对寄存器的运算速度比对主存的运算速度快得多。这是因为CPU访问寄存器的速度比主存快得多。

缓存:由于计算机的存储设备和处理器的运算速度之间存在几个数量级的差距,现代计算机系统不得不增加一层读写速度尽可能接近处理器运算速度的缓存,作为内存和处理器之间的缓冲区:将运算所需的数据复制到缓存中,这样运算才能快速进行, 然后在操作完成时从缓存同步到内存,这样处理器就不必等待缓慢的内存读写。 中央处理器访问缓存层的速度比主存储器快,但通常比内部寄存器慢。每个CPU可能有一个CPU缓存层,有些CPU也有多个缓存。在某个时刻,一个或多个高速缓存行可以被读入高速缓存,并且一个或多个高速缓存行可以被刷新回主存储器。

内存:计算机也包含一个主内存。所有的中央处理器都可以访问主存。主内存通常比中央处理器中的缓存大得多。

工作原理:正常情况下,当一个CPU需要读取主存时,会将部分主存读入CPU缓存。它甚至可以将缓存中的部分内容读入其内部寄存器,然后在寄存器中执行操作。当CPU需要将结果写回主存时,它会将内部寄存器的值刷新到缓存中,然后在某个时间点将该值刷新回主存。

一些问题:(尤其是多线程环境下)

缓存一致性问题:在多处理器系统中,每个处理器都有自己的缓存

它们又共享同一主内存(MainMemory)。基于高速缓存的存储交互很好地解决了处理器与内存的速度矛盾,但是也引入了新的问题:缓存一致性(CacheCoherence)。当多个处理器的运算任务都涉及同一块主内存区域时,将可能导致各自的缓存数据不一致的情况,如果真的发生这种情况,那同步回到主内存时以谁的缓存数据为准呢?为了解决一致性的问题,需要各个处理器访问缓存时都遵循一些协议,在读写时要根据协议来进行操作,这类协议有MSI、MESI(IllinoisProtocol)、MOSI、Synapse、Firefly及DragonProtocol,等等: Java内存模型指的是什么

  • 指令重排序问题:为了使得处理器内部的运算单元能尽量被充分利用,处理器可能会对输入代码进行乱序执行(Out-Of-Order Execution)优化,处理器会在计算之后将乱序执行的结果重组,保证该结果与顺序执行的结果是一致的,但并不保证程序中各个语句计算的先后顺序与输入代码中的顺序一致。因此,如果存在一个计算任务依赖另一个计算任务的中间结果,那么其顺序性并不能靠代码的先后顺序来保证。与处理器的乱序执行优化类似,Java虚拟机的即时编译器中也有类似的指令重排序(Instruction Reorder)优化

  • Java内存模型和硬件内存架构之间的桥接

    Java内存模型与硬件内存架构之间存在差异。硬件内存架构没有区分线程栈和堆。对于硬件,所有的线程栈和堆都分布在主内存中。部分线程栈和堆可能有时候会出现在CPU缓存中和CPU内部的寄存器中。如下图所示: Java内存模型指的是什么

    • 从抽象的角度来看,JMM定义了线程和主内存之间的抽象关系:

    • 线程之间的共享变量存储在主内存(Main Memory)中

    • 每个线程都有一个私有的本地内存(Local Memory),本地内存是JMM的一个抽象概念,并不真实存在,它涵盖了缓存、写缓冲区、寄存器以及其他的硬件和编译器优化。本地内存中存储了该线程以读/写共享变量的拷贝副本。

    • 从更低的层次来说,主内存就是硬件的内存,而为了获取更好的运行速度,虚拟机及硬件系统可能会让工作内存优先存储于寄存器和高速缓存中。

    • Java内存模型中的线程的工作内存(working memory)是cpu的寄存器和高速缓存的抽象描述。而JVM的静态内存储模型(JVM内存模型)只是一种对内存的物理划分而已,它只局限在内存,而且只局限在JVM的内存。 Java内存模型指的是什么

    Java内存模型指的是什么

    JMM模型下的线程间通信:

    线程间通信必须要经过主内存。 如下,如果线程A与线程B之间要通信的话,必须要经历下面2个步骤:

    1)线程A把本地内存A中更新过的共享变量刷新到主内存中去。

    2)线程B到主内存中去读取线程A之前已更新过的共享变量。 Java内存模型指的是什么

    关于主内存与工作内存之间的具体交互协议,即一个变量如何从主内存拷贝到工作内存、如何从工作内存同步到主内存之间的实现细节,Java内存模型定义了以下八种操作来完成:

    • lock(锁定):作用于主内存的变量,把一个变量标识为一条线程独占状态。

    • unlock(解锁):作用于主内存变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。

    • read(读取):作用于主内存变量,把一个变量值从主内存传输到线程的工作内存中,以便随后的load动作使用

    • load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中。

    • use(使用):作用于工作内存的变量,把工作内存中的一个变量值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时将会执行这个操作。

    • assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋值给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。

    • store(存储):作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中,以便随后的write的操作。

    • write(写入):作用于主内存的变量,它把store操作从工作内存中一个变量的值传送到主内存的变量中。

    Java内存模型还规定了在执行上述八种基本操作时,必须满足如下规则:

    • 如果要把一个变量从主内存中复制到工作内存,就需要按顺序地执行read和load操作, 如果把变量从工作内存中同步回主内存中,就要按顺序地执行store和write操作。但Java内存模型只要求上述操作必须按顺序执行,而没有保证必须是连续执行。

    • 不允许read和load、store和write操作之一单独出现

    • 不允许一个线程丢弃它的最近assign的操作,即变量在工作内存中改变了之后必须同步到主内存中。

    • 不允许一个线程无原因地(没有发生过任何assign操作)把数据从工作内存同步回主内存中。

    • 一个新的变量只能在主内存中诞生,不允许在工作内存中直接使用一个未被初始化(load或assign)的变量。即就是对一个变量实施use和store操作之前,必须先执行过了assign和load操作。

    • 一个变量在同一时刻只允许一条线程对其进行lock操作,但lock操作可以被同一条线程重复执行多次,多次执行lock后,只有执行相同次数的unlock操作,变量才会被解锁。即lock和unlock必须成对出现!

    • 如果对一个变量执行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前需要重新执行load或assign操作初始化变量的值

    • 如果一个变量事先没有被lock操作锁定,则不允许对它执行unlock操作;也不允许去unlock一个被其他线程锁定的变量。

    • 对一个变量执行unlock操作之前,必须先把此变量同步到主内存中(执行store和write操作)。

    Java内存模型解决的问题

    当对象和变量被存放在计算机中各种不同的内存区域中时,就可能会出现一些具体的问题。Java内存模型建立所围绕的问题:在多线程并发过程中,如何处理多线程读同步问题与可见性(多线程缓存与指令重排序)、多线程写同步问题与原子性(多线程竞争race condition)。

    1、多线程读同步与可见性

    可见性(共享对象可见性):线程对共享变量修改的可见性。当一个线程修改了共享变量的值,其他线程能够立刻得知这个修改

    2、线程缓存导致的可见性问题:

    如果两个或者更多的线程在没有正确的使用volatile声明或者同步的情况下共享一个对象,一个线程更新这个共享对象可能对其它线程来说是不可见的:共享对象被初始化在主存中。跑在CPU上的一个线程将这个共享对象读到CPU缓存中,然后修改了这个对象。只要CPU缓存没有被刷新会主存,对象修改后的版本对跑在其它CPU上的线程都是不可见的。这种方式可能导致每个线程拥有这个共享对象的私有拷贝,每个拷贝停留在不同的CPU缓存中。

    下图示意了这种情形。跑在左边CPU的线程拷贝这个共享对象到它的CPU缓存中,然后将count变量的值修改为2。这个修改对跑在右边CPU上的其它线程是不可见的,因为修改后的count的值还没有被刷新回主存中去。 Java内存模型指的是什么

    解决这个内存可见性问题你可以使用:

    • Java中的volatile关键字:volatile关键字可以保证直接从主存中读取一个变量,如果这个变量被修改后,总是会被写回到主存中去。Java内存模型是通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值这种依赖主内存作为传递媒介的方式来实现可见性的,无论是普通变量还是volatile变量都是如此,普通变量与volatile变量的区别是:volatile的特殊规则保证了新值能立即同步到主内存,以及每个线程在每次使用volatile变量前都立即从主内存刷新。因此我们可以说volatile保证了多线程操作时变量的可见性,而普通变量则不能保证这一点,虽然它最终也会同步到主存,但这个时候可能有其他线程使用的旧的“脏数据”。

    • Java中的synchronized关键字:同步块的可见性是由“如果对一个变量执行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前需要重新执行load或assign操作初始化变量的值”、“对一个变量执行unlock操作之前,必须先把此变量同步回主内存中(执行store和write操作)”这两条规则获得的。

    • Java中的final关键字:final关键字的可见性是指,被final修饰的字段在构造器中一旦被初始化完成,并且构造器没有把“this”的引用传递出去(this引用逃逸是一件很危险的事情,其他线程有可能通过这个引用访问到“初始化了一半”的对象),那么在其他线程就能看见final字段的值(无须同步)

    3、重排序导致的可见性问题:

    Java程序中天然的有序性可以总结为一句话:如果在本地线程内观察,所有操作都是有序的(“线程内表现为串行”(Within-Thread As-If-Serial Semantics));如果在一个线程中观察另一个线程,所有操作都是无序的(“指令重排序”现象和“线程工作内存与主内存同步延迟”现象)。

    Java语言提供了volatile和synchronized两个关键字来保证线程之间操作的有序性:

    • volatile关键字本身就包含了禁止指令重排序的语义

    • synchronized则是由“一个变量在同一个时刻只允许一条线程对其进行lock操作”这条规则获得的,这个规则决定了持有同一个锁的两个同步块只能串行地进入

    指令序列的重排序:

    1)编译器优化的重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。

    2)指令级并行的重排序。现代处理器采用了指令级并行技术(Instruction-LevelParallelism,ILP)来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。

    3)内存系统的重排序。由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。 Java内存模型指的是什么

    每个处理器上的写缓冲区,仅仅对它所在的处理器可见。这会导致处理器执行内存操作的顺序可能会与内存实际的操作执行顺序不一致。由于现代的处理器都会使用写缓冲区,因此现代的处理器都会允许对写-读操作进行重排序: Java内存模型指的是什么

    数据依赖: 编译器和处理器在重排序时,会遵守数据依赖性,编译器和处理器不会改变存在数据依赖关系的两个操作的执行顺序。(这里所说的数据依赖性仅针对单个处理器中执行的指令序列和单个线程中执行的操作,不同处理器之间和不同线程之间的数据依赖性不被编译器和处理器考虑) Java内存模型指的是什么

    指令重排序对内存可见性的影响

    Java内存模型指的是什么

    当1和2之间没有数据依赖关系时,1和2之间就可能被重排序(3和4类似)。这样的结果就是:读线程B执行4时,不一定能看到写线程A在执行1时对共享变量的修改。

    指令重排序改变多线程程序的执行结果例子: Java内存模型指的是什么

    flag变量是个标记,用来标识变量a是否已被写入。这里假设有两个线程A和B,A首先执行writer()方法,随后B线程接着执行reader()方法。线程B在执行操作4时,能否看到线程A在操作1对共享变量a的写入呢?

    答案是:不一定能看到。

    由于操作1和操作2没有数据依赖关系,编译器和处理器可以对这两个操作重排序;同样,操作3和操作4没有数据依赖关系,编译器和处理器也可以对这两个操作重排序。

    as-if-serial语义:

    不管怎么重排序(编译器和处理器为了提高并行度),(单线程)程序的执行结果不能被改变。(编译器、runtime和处理器都必须遵守as-if-serial语义)

    happens before:

    从JDK 5开始,Java使用新的JSR-133内存模型,JSR-133使用happens-before的概念来阐述操作之间的内存可见性:在JMM中,如果一个操作执行的结果需要对另一个操作可见(两个操作既可以是在一个线程之内,也可以是在不同线程之间),那么这两个操作之间必须要存在happens-before关系:

    • 程序顺序规则:一个线程中的每个操作,happens-before于该线程中的任意后续操作。

    • 监视器锁规则:对一个锁的解锁,happens-before于随后对这个锁的加锁。

    • volatile变量规则:对一个volatile域的写,happens-before于任意后续对这个volatile域的读。

    • 传递性:如果A happens-before B,且B happens-before C,那么A happens-before C。

    一个happens-before规则对应于一个或多个编译器和处理器重排序规则

    内存屏障禁止特定类型的处理器重排序:

    重排序可能会导致多线程程序出现内存可见性问题。对于处理器重排序,JMM的处理器重排序规则会要求Java编译器在生成指令序列时,插入特定类型的内存屏障(Memory Barriers,Intel称之为Memory Fence)指令,通过内存屏障指令来禁止特定类型的处理器重排序。通过禁止特定类型的编译器重排序和处理器重排序,为程序员提供一致的内存可见性保证。

    为了保证内存可见性,Java编译器在生成指令序列的适当位置会插入内存屏障指令来禁止特定类型的处理器重排序。 Java内存模型指的是什么

    StoreLoad Barriers是一个“全能型”的屏障,它同时具有其他3个屏障的效果。现代的多处理器大多支持该屏障(其他类型的屏障不一定被所有处理器支持)。执行该屏障开销会很昂贵,因为当前处理器通常要把写缓冲区中的数据全部刷新到内存中(Buffer Fully Flush)。

    多线程写同步与原子性

    多线程竞争(Race Conditions)问题:当读,写和检查共享变量时出现race conditions。

    如果两个或者更多的线程共享一个对象,多个线程在这个共享对象上更新变量,就有可能发生race conditions。

    想象一下,如果线程A读一个共享对象的变量count到它的CPU缓存中。再想象一下,线程B也做了同样的事情,但是往一个不同的CPU缓存中。现在线程A将count加1,线程B也做了同样的事情。现在count已经被增加了两次,每个CPU缓存中一次。如果这些增加操作被顺序的执行,变量count应该被增加两次,然后原值+2被写回到主存中去。然而,两次增加都是在没有适当的同步下并发执行的。无论是线程A还是线程B将count修改后的版本写回到主存中取,修改后的值仅会被原值大1,尽管增加了两次:

    Java内存模型指的是什么

    解决这个问题可以使用Java同步块。一个同步块可以保证在同一时刻仅有一个线程可以进入代码的临界区。同步块还可以保证代码块中所有被访问的变量将会从主存中读入,当线程退出同步代码块时,所有被更新的变量都会被刷新回主存中去,不管这个变量是否被声明为volatile。

    使用原子性保证多线程写同步问题:

    原子性:指一个操作是按原子的方式执行的。要么该操作不被执行;要么以原子方式执行,即执行过程中不会被其它线程中断。 Java内存模型指的是什么

    实现原子性:

    • 由Java内存模型来直接保证的原子性变量操作包括read、load、assign、use、store、write,我们大致可以认为基本数据类型变量、引用类型变量、声明为volatile的任何类型变量的访问读写是具备原子性的(long和double的非原子性协定:对于64位的数据,如long和double,Java内存模型规范允许虚拟机将没有被volatile修饰的64位数据的读写操作划分为两次32位的操作来进行,即允许虚拟机实现选择可以不保证64位数据类型的load、store、read和write这四个操作的原子性,即如果有多个线程共享一个并未声明为volatile的long或double类型的变量,并且同时对它们进行读取和修改操作,那么某些线程可能会读取到一个既非原值,也不是其他线程修改值的代表了“半个变量”的数值。但由于目前各种平台下的商用虚拟机几乎都选择把64位数据的读写操作作为原子操作来对待,因此在编写代码时一般也不需要将用到的long和double变量专门声明为volatile)。这些类型变量的读、写天然具有原子性,但类似于 “基本变量++” / “volatile++” 这种复合操作并没有原子性。

    • 如果应用场景需要一个更大范围的原子性保证,需要使用同步块技术。Java内存模型提供了lock和unlock操作来满足这种需求。虚拟机提供了字节码指令monitorenter和monitorexist来隐式地使用这两个操作,这两个字节码指令反映到Java代码中就是同步快——synchronized关键字。

    JMM对特殊Java语义的特殊规则支持

    volatile总结 (保证内存可见性:Lock前缀的指令、内存屏障禁止重排序)

    synchronized总结 (保证内存可见性和操作原子性:互斥锁;锁优化)

    “Java内存模型指的是什么”的内容就介绍到这里了,感谢大家的阅读。如果想了解更多行业相关的知识可以关注网站,小编将为大家输出更多高质量的实用文章!

    内容来源网络,如有侵权,联系删除,本文地址:https://www.230890.com/zhan/115175.html

    (0)

    相关推荐

    • 如何将Tibco Ent .对于(同JavaMessageService)Java消息服务代替JBoss-3.0.1RC1_Tomcat-4.0.4中的JBossMQ

      技术如何将Tibco Ent. for JMS代替JBoss-3.0.1RC1_Tomcat-4.0.4中的JBossMQ小编给大家分享一下如何将Tibco Ent. for JMS代替JBoss-3.0.1RC1_To

      攻略 2021年12月24日
    • css样式的继承性、层叠性 、优先级有什么作用

      技术css样式的继承性、层叠性 、优先级有什么作用这篇文章主要讲解了“css样式的继承性、层叠性 、优先级有什么作用”,文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习“css样

      攻略 2021年12月10日
    • python如何链接数据库

      技术python如何链接数据库小编给大家分享一下python如何链接数据库,相信大部分人都还不怎么了解,因此分享这篇文章给大家参考一下,希望大家阅读完这篇文章后大有收获,下面让我们一起去了解一下吧!MySQLdb模块是为

      攻略 2021年11月24日
    • impunity,犬儒主义到底是什么意思

      技术impunity,犬儒主义到底是什么意思犬儒主义者百科名片“犬儒主义”一般认为是苏格拉底的弟子安提斯泰尼创立的,另一人物第欧根尼则因为住在木桶里的怪异行为而成为更有名的犬儒主义者。当时奉行这一主义的哲学家或思想家,他

      生活 2021年10月30日
    • 格拼音,占上中下格的拼音有哪些

      技术格拼音,占上中下格的拼音有哪些拼音格,它是有4条横着的平行线从上到下依次组成了上格、中格和下格格拼音。 在四线三格占上中格的小写字母有9个:b、d、f、h、i、k、l、t、ü,注意它们书写在中上格时,笔画上端必须紧顶

      生活 2021年10月25日
    • 怎么用CSS实现仿Windows10鼠标照亮边框效果

      技术怎么用CSS实现仿Windows10鼠标照亮边框效果这篇文章主要讲解了“怎么用CSS实现仿Windows10鼠标照亮边框效果”,文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学

      攻略 2021年11月6日