精华内容
下载资源
问答
  • 计算机内存模型概念

    万次阅读 多人点赞 2017-10-27 14:38:39
    由于程序运行过程中的临时数据是存放在主存(物理内存)当中的,这时就存在一个问题,由于CPU执行速度很快,而从内存读取数据和向内存写入数据的过程跟CPU执行指令的速度比起来要慢的多,因此如果任何时候对数据的...

    一.内存模型的相关概念

      大家都知道,计算机在执行程序时,每条指令都是在CPU中执行的,而执行指令过程中,势必涉及到数据的读取和写入。由于程序运行过程中的临时数据是存放在主存(物理内存)当中的,这时就存在一个问题,由于CPU执行速度很快,而从内存读取数据和向内存写入数据的过程跟CPU执行指令的速度比起来要慢的多,因此如果任何时候对数据的操作都要通过和内存的交互来进行,会大大降低指令执行的速度。因此在CPU里面就有了高速缓存。

      也就是,当程序在运行过程中,会将运算需要的数据从主存复制一份到CPU的高速缓存当中,那么CPU进行计算时就可以直接从它的高速缓存读取数据和向其中写入数据,当运算结束之后,再将高速缓存中的数据刷新到主存当中。举个简单的例子,比如下面的这段代码:

    1
    i = i +  1 ;

       当线程执行这个语句时,会先从主存当中读取i的值,然后复制一份到高速缓存当中,然后CPU执行指令对i进行加1操作,然后将数据写入高速缓存,最后将高速缓存中i最新的值刷新到主存当中。

      这个代码在单线程中运行是没有任何问题的,但是在多线程中运行就会有问题了。在多核CPU中,每条线程可能运行于不同的CPU中,因此每个线程运行时有自己的高速缓存(对单核CPU来说,其实也会出现这种问题,只不过是以线程调度的形式来分别执行的)。本文我们以多核CPU为例。

      比如同时有2个线程执行这段代码,假如初始时i的值为0,那么我们希望两个线程执行完之后i的值变为2。但是事实会是这样吗?

      可能存在下面一种情况:初始时,两个线程分别读取i的值存入各自所在的CPU的高速缓存当中,然后线程1进行加1操作,然后把i的最新值1写入到内存。此时线程2的高速缓存当中i的值还是0,进行加1操作之后,i的值为1,然后线程2把i的值写入内存。

      最终结果i的值是1,而不是2。这就是著名的缓存一致性问题。通常称这种被多个线程访问的变量为共享变量。

      也就是说,如果一个变量在多个CPU中都存在缓存(一般在多线程编程时才会出现),那么就可能存在缓存不一致的问题。

      为了解决缓存不一致性问题,通常来说有以下2种解决方法:

      1)通过在总线加LOCK#锁的方式

      2)通过缓存一致性协议

      这2种方式都是硬件层面上提供的方式。

      在早期的CPU当中,是通过在总线上加LOCK#锁的形式来解决缓存不一致的问题。因为CPU和其他部件进行通信都是通过总线来进行的,如果对总线加LOCK#锁的话,也就是说阻塞了其他CPU对其他部件访问(如内存),从而使得只能有一个CPU能使用这个变量的内存。比如上面例子中 如果一个线程在执行 i = i +1,如果在执行这段代码的过程中,在总线上发出了LCOK#锁的信号,那么只有等待这段代码完全执行完毕之后,其他CPU才能从变量i所在的内存读取变量,然后进行相应的操作。这样就解决了缓存不一致的问题。

      但是上面的方式会有一个问题,由于在锁住总线期间,其他CPU无法访问内存,导致效率低下。

      所以就出现了缓存一致性协议。最出名的就是Intel 的MESI协议,MESI协议保证了每个缓存中使用的共享变量的副本是一致的。它核心的思想是:当CPU写数据时,如果发现操作的变量是共享变量,即在其他CPU中也存在该变量的副本,会发出信号通知其他CPU将该变量的缓存行置为无效状态,因此当其他CPU需要读取这个变量时,发现自己缓存中缓存该变量的缓存行是无效的,那么它就会从内存重新读取。

    二.并发编程中的三个概念

      在并发编程中,我们通常会遇到以下三个问题:原子性问题,可见性问题,有序性问题。我们先看具体看一下这三个概念:

    1.原子性

      原子性:即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。

      一个很经典的例子就是银行账户转账问题:

      比如从账户A向账户B转1000元,那么必然包括2个操作:从账户A减去1000元,往账户B加上1000元。

      试想一下,如果这2个操作不具备原子性,会造成什么样的后果。假如从账户A减去1000元之后,操作突然中止。然后又从B取出了500元,取出500元之后,再执行 往账户B加上1000元 的操作。这样就会导致账户A虽然减去了1000元,但是账户B没有收到这个转过来的1000元。

      所以这2个操作必须要具备原子性才能保证不出现一些意外的问题。

      同样地反映到并发编程中会出现什么结果呢?

      举个最简单的例子,大家想一下假如为一个32位的变量赋值过程不具备原子性的话,会发生什么后果?

    1
    i =  9 ;

       假若一个线程执行到这个语句时,我暂且假设为一个32位的变量赋值包括两个过程:为低16位赋值,为高16位赋值。

      那么就可能发生一种情况:当将低16位数值写入之后,突然被中断,而此时又有一个线程去读取i的值,那么读取到的就是错误的数据。

    2.可见性

      可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。

      举个简单的例子,看下面这段代码:

    1
    2
    3
    4
    5
    6
    //线程1执行的代码
    int  i =  0 ;
    i =  10 ;
     
    //线程2执行的代码
    j = i;

       假若执行线程1的是CPU1,执行线程2的是CPU2。由上面的分析可知,当线程1执行 i =10这句时,会先把i的初始值加载到CPU1的高速缓存中,然后赋值为10,那么在CPU1的高速缓存当中i的值变为10了,却没有立即写入到主存当中。

      此时线程2执行 j = i,它会先去主存读取i的值并加载到CPU2的缓存当中,注意此时内存当中i的值还是0,那么就会使得j的值为0,而不是10.

      这就是可见性问题,线程1对变量i修改了之后,线程2没有立即看到线程1修改的值。

    3.有序性

      有序性:即程序执行的顺序按照代码的先后顺序执行。举个简单的例子,看下面这段代码:

    1
    2
    3
    4
    int  i =  0 ;              
    boolean  flag =  false ;
    i =  1 ;                 //语句1  
    flag =  true ;           //语句2

       上面代码定义了一个int型变量,定义了一个boolean类型变量,然后分别对两个变量进行赋值操作。从代码顺序上看,语句1是在语句2前面的,那么JVM在真正执行这段代码的时候会保证语句1一定会在语句2前面执行吗?不一定,为什么呢?这里可能会发生指令重排序(Instruction Reorder)。

      下面解释一下什么是指令重排序,一般来说,处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。

      比如上面的代码中,语句1和语句2谁先执行对最终的程序结果并没有影响,那么就有可能在执行过程中,语句2先执行而语句1后执行。

      但是要注意,虽然处理器会对指令进行重排序,但是它会保证程序最终结果会和代码顺序执行结果相同,那么它靠什么保证的呢?再看下面一个例子:

    1
    2
    3
    4
    int  a =  10 ;     //语句1
    int  r =  2 ;     //语句2
    a = a +  3 ;     //语句3
    r = a*a;      //语句4

       这段代码有4个语句,那么可能的一个执行顺序是:

      

      

      那么可不可能是这个执行顺序呢: 语句2   语句1    语句4   语句3

      不可能,因为处理器在进行重排序时是会考虑指令之间的数据依赖性,如果一个指令Instruction 2必须用到Instruction 1的结果,那么处理器会保证Instruction 1会在Instruction 2之前执行。

      虽然重排序不会影响单个线程内程序执行的结果,但是多线程呢?下面看一个例子:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    //线程1:
    context = loadContext();    //语句1
    inited =  true ;              //语句2
     
    //线程2:
    while (!inited ){
       sleep()
    }
    doSomethingwithconfig(context);

       上面代码中,由于语句1和语句2没有数据依赖性,因此可能会被重排序。假如发生了重排序,在线程1执行过程中先执行语句2,而此是线程2会以为初始化工作已经完成,那么就会跳出while循环,去执行doSomethingwithconfig(context)方法,而此时context并没有被初始化,就会导致程序出错。

       从上面可以看出,指令重排序不会影响单个线程的执行,但是会影响到线程并发执行的正确性。

      也就是说,要想并发程序正确地执行,必须要保证原子性、可见性以及有序性。只要有一个没有被保证,就有可能会导致程序运行不正确。

    三.Java内存模型

      在前面谈到了一些关于内存模型以及并发编程中可能会出现的一些问题。下面我们来看一下Java内存模型,研究一下Java内存模型为我们提供了哪些保证以及在java中提供了哪些方法和机制来让我们在进行多线程编程时能够保证程序执行的正确性。

      在Java虚拟机规范中试图定义一种Java内存模型(Java Memory Model,JMM)来屏蔽各个硬件平台和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的内存访问效果。那么Java内存模型规定了哪些东西呢,它定义了程序中变量的访问规则,往大一点说是定义了程序执行的次序。注意,为了获得较好的执行性能,Java内存模型并没有限制执行引擎使用处理器的寄存器或者高速缓存来提升指令执行速度,也没有限制编译器对指令进行重排序。也就是说,在java内存模型中,也会存在缓存一致性问题和指令重排序的问题。

      Java内存模型规定所有的变量都是存在主存当中(类似于前面说的物理内存),每个线程都有自己的工作内存(类似于前面的高速缓存)。线程对变量的所有操作都必须在工作内存中进行,而不能直接对主存进行操作。并且每个线程不能访问其他线程的工作内存。

      举个简单的例子:在java中,执行下面这个语句:

    1
    i  =  10 ;

       执行线程必须先在自己的工作线程中对变量i所在的缓存行进行赋值操作,然后再写入主存当中。而不是直接将数值10写入主存当中。

      那么Java语言 本身对 原子性、可见性以及有序性提供了哪些保证呢?

    1.原子性

      在Java中,对基本数据类型的变量的读取和赋值操作是原子性操作,即这些操作是不可被中断的,要么执行,要么不执行。

      上面一句话虽然看起来简单,但是理解起来并不是那么容易。看下面一个例子i:

      请分析以下哪些操作是原子性操作:

    1
    2
    3
    4
    x =  10 ;          //语句1
    y = x;          //语句2
    x++;            //语句3
    x = x +  1 ;      //语句4

       咋一看,有些朋友可能会说上面的4个语句中的操作都是原子性操作。其实只有语句1是原子性操作,其他三个语句都不是原子性操作。

      语句1是直接将数值10赋值给x,也就是说线程执行这个语句的会直接将数值10写入到工作内存中。

      语句2实际上包含2个操作,它先要去读取x的值,再将x的值写入工作内存,虽然读取x的值以及 将x的值写入工作内存 这2个操作都是原子性操作,但是合起来就不是原子性操作了。

      同样的,x++和 x = x+1包括3个操作:读取x的值,进行加1操作,写入新的值。

       所以上面4个语句只有语句1的操作具备原子性。

      也就是说,只有简单的读取、赋值(而且必须是将数字赋值给某个变量,变量之间的相互赋值不是原子操作)才是原子操作。

      不过这里有一点需要注意:在32位平台下,对64位数据的读取和赋值是需要通过两个操作来完成的,不能保证其原子性。但是好像在最新的JDK中,JVM已经保证对64位数据的读取和赋值也是原子性操作了。

      从上面可以看出,Java内存模型只保证了基本读取和赋值是原子性操作,如果要实现更大范围操作的原子性,可以通过synchronized和Lock来实现。由于synchronized和Lock能够保证任一时刻只有一个线程执行该代码块,那么自然就不存在原子性问题了,从而保证了原子性。

    2.可见性

      对于可见性,Java提供了volatile关键字来保证可见性。

      当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。

      而普通的共享变量不能保证可见性,因为普通共享变量被修改之后,什么时候被写入主存是不确定的,当其他线程去读取时,此时内存中可能还是原来的旧值,因此无法保证可见性。

      另外,通过synchronized和Lock也能够保证可见性,synchronized和Lock能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中。因此可以保证可见性。

    3.有序性

      在Java内存模型中,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。

      在Java里面,可以通过volatile关键字来保证一定的“有序性”(具体原理在下一节讲述)。另外可以通过synchronized和Lock来保证有序性,很显然,synchronized和Lock保证每个时刻是有一个线程执行同步代码,相当于是让线程顺序执行同步代码,自然就保证了有序性。

      另外,Java内存模型具备一些先天的“有序性”,即不需要通过任何手段就能够得到保证的有序性,这个通常也称为 happens-before 原则。如果两个操作的执行次序无法从happens-before原则推导出来,那么它们就不能保证它们的有序性,虚拟机可以随意地对它们进行重排序。

      下面就来具体介绍下happens-before原则(先行发生原则):

    • 程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作
    • 锁定规则:一个unLock操作先行发生于后面对同一个锁额lock操作
    • volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作
    • 传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C
    • 线程启动规则:Thread对象的start()方法先行发生于此线程的每个一个动作
    • 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生
    • 线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行
    • 对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始

      这8条原则摘自《深入理解Java虚拟机》。

      这8条规则中,前4条规则是比较重要的,后4条规则都是显而易见的。

      下面我们来解释一下前4条规则:

      对于程序次序规则来说,我的理解就是一段程序代码的执行在单个线程中看起来是有序的。注意,虽然这条规则中提到“书写在前面的操作先行发生于书写在后面的操作”,这个应该是程序看起来执行的顺序是按照代码顺序执行的,因为虚拟机可能会对程序代码进行指令重排序。虽然进行重排序,但是最终执行的结果是与程序顺序执行的结果一致的,它只会对不存在数据依赖性的指令进行重排序。因此,在单个线程中,程序执行看起来是有序执行的,这一点要注意理解。事实上,这个规则是用来保证程序在单线程中执行结果的正确性,但无法保证程序在多线程中执行的正确性。

      第二条规则也比较容易理解,也就是说无论在单线程中还是多线程中,同一个锁如果出于被锁定的状态,那么必须先对锁进行了释放操作,后面才能继续进行lock操作。

      第三条规则是一条比较重要的规则,也是后文将要重点讲述的内容。直观地解释就是,如果一个线程先去写一个变量,然后一个线程去进行读取,那么写入操作肯定会先行发生于读操作。

      第四条规则实际上就是体现happens-before原则具备传递性。

    展开全文
  • Spark 介绍(基于内存计算的大数据并行计算框架)  Hadoop与Spark 行业广泛使用Hadoop来分析他们的数据集。原因是Hadoop框架基于一个简单的编程模型(MapReduce),它支持可扩展,灵活,容错和成本...

    Spark 介绍(基于内存计算的大数据并行计算框架)


     Hadoop与Spark

    行业广泛使用Hadoop来分析他们的数据集。原因是Hadoop框架基于一个简单的编程模型(MapReduce),它支持可扩展,灵活,容错和成本有效的计算解决方案。这里,主要关注的是在处理大型数据集时在查询之间的等待时间和运行程序的等待时间方面保持速度。
    Spark由Apache Software Foundation引入,用于加速Hadoop计算软件过程。
    对于一个普遍的信念, Spark不是Hadoop的修改版本,并不是真的依赖于Hadoop,因为它有自己的集群管理。 Hadoop只是实现Spark的方法之一。
    Spark以两种方式使用Hadoop - 一个是 存储,另一个是 处理。由于Spark具有自己的集群管理计算,因此它仅使用Hadoop进行存储。

    Apache Spark简介

    Apache Spark是一种快速的集群计算技术,专为快速计算而设计。它基于Hadoop MapReduce,它扩展了MapReduce模型,以有效地将其用于更多类型的计算,包括交互式查询和流处理。 Spark的主要特性是它的 内存中集群计算,提高了应用程序的处理速度。
    Spark旨在涵盖各种工作负载,如批处理应用程序,迭代算法,交互式查询和流式处理。除了在相应系统中支持所有这些工作负载之外,它还减少了维护单独工具的管理负担。

    Apache Spark的演变

    Spark是Hadoop在2009年在加州大学伯克利分校的Matei Zaharia的AMPLab开发的子项目之一。它是在2010年根据BSD许可开放。它在2013年捐赠给Apache软件基金会,现在Apache Spark已经成为2014年2月的顶级Apache项目。

    Apache Spark的特性

    Apache Spark具有以下功能。

    速度

    Spark有助于在Hadoop集群中运行应用程序,在内存中速度提高100倍,在磁盘上运行时提高10倍。这可以通过减少对磁盘的读/写操作的数量来实现。它将中间处理数据存储在存储器中。

    支持多种语言

    Spark在Java,Scala或Python中提供了内置的API。因此,您可以使用不同的语言编写应用程序。 Spark提供了80个高级操作员进行交互式查询。

    高级分析

    Spark不仅支持“Map”和“reduce”。它还支持SQL查询,流数据,机器学习(ML)和图算法。

    Spark基于Hadoop

    下图显示了如何使用Hadoop组件构建Spark的三种方式。

     

    星火内置在Hadoop

     

    Spark部署有三种方式,如下所述。
    Standalone- Spark独立部署意味着Spark占据HDFS(Hadoop分布式文件系统)顶部的位置,并明确为HDFS分配空间。 这里,Spark和MapReduce将并行运行以覆盖集群上的所有spark作业。
    Hadoop Yarn- Hadoop Yarn部署意味着,spark只需运行在Yarn上,无需任何预安装或根访问。 它有助于将Spark集成到Hadoop生态系统或Hadoop堆栈中。 它允许其他组件在堆栈顶部运行。
    Spark in MapReduce (SIMR) - MapReduce中的Spark用于在独立部署之外启动spark job。 使用SIMR,用户可以启动Spark并使用其shell而无需任何管理访问。

    Spark的组件

    下图说明了Spark的不同组件。
    星火组件

     

     

    Apache Spark Core

    Spark Core是spark平台的基础通用执行引擎,所有其他功能都是基于。它在外部存储系统中提供内存计算和引用数据集。
    Spark SQL
    Spark SQL是Spark Core之上的一个组件,它引入了一个称为SchemaRDD的新数据抽象,它为结构化和半结构化数据提供支持。

    Spark Streaming

    Spark Streaming利用Spark Core的快速调度功能来执行流式分析。它以小批量获取数据,并对这些小批量的数据执行RDD(弹性分布式数据集)转换。

    MLlib (Machine Learning Library)

    MLlib是Spark之上的分布式机器学习框架,因为基于分布式内存的Spark架构。根据基准,它是由MLlib开发人员针对交替最小二乘法(ALS)实现完成的。 Spark MLlib是基于Hadoop磁盘的 Apache Mahout版本的9倍(在Mahout获得了Spark接口之前)。
    GraphX
    GraphX是Spark上的一个分布式图形处理框架。它提供了一个用于表达图形计算的API,可以通过使用Pregel抽象API为用户定义的图形建模。它还为此抽象提供了一个优化的运行时。

    展开全文
  • 如果服务器有足够的空闲内存,页就会被留在工作集中,当自由内存少于一个特定的阀值,页就会被清除出工作集.(类似于:GC)内存页分为:文件页和计算页 内存中的文件页是文件缓存区,即文件型的内存页,用于存放文件数据...

    关于Windows下,Perfmon(性能技术器)中Process(Work Set)的理解:

    处理线程最近使用的内存页,反映了每一个进程使用的内存页的数量.如果服务器有足够的空闲内存,页就会被留在工作集中,当自由内存少于一个特定的阀值,页就会被清除出工作集.(类似于:GC)

    内存页分为:文件页和计算页
        内存中的文件页是文件缓存区,即文件型的内存页,用于存放文件数据的内存页(也称永久页),作用在于读写文件时可以减少对磁盘的访问,如果它的大小设置得太小,会引起系统频繁地访问磁盘,增加磁盘I/O;设置太大,会浪费内存资源。
        内存中的计算页也称为计算型的内存页,主要用于存放程序代码和临时使用的数据。 

    展开全文
  • 计算机内存体系与Java 内存模型

    千次阅读 2019-05-26 15:39:41
    让计算机并发执行若干个任务与更充分的利用计算机处理器的效能之间的因果关系,非常的复杂,其中一个重要的复杂性来源是绝大多数的运算任务都不可能只靠处理器计算就能完成的,处理器至少要与内存交互,如:读取运算...

    计算机内存相关硬件介绍与缓存一致性:

    让计算机并发执行若干个任务与更充分的利用计算机处理器的效能之间的因果关系,非常的复杂,其中一个重要的复杂性来源是绝大多数的运算任务都不可能只靠处理器计算就能完成的,处理器至少要与内存交互,如:读取运算数据,存储运算结果等。这个IO操作是很难消除的(无法仅靠寄存器来完成所有的运算任务)。由于计算机的存储设备与处理器的运算速度有几个数量级的差距,所以现代计算机系统都不得不加入一层读写速度尽可能接近处理器运算速度的高速缓存来作为内存与处理器之间的缓冲:将运算需要使用到的数据复制到缓冲中,让运算能快速进行,当运算结束后再从缓存同步回主内存之中,这样处理器就无需等待缓慢的内存读写了。

     

    这里真是又不得不搬出大学时枯燥无聊的计算机系统结构了,真心后悔那会听得云里雾里打瞌睡啊~~

    觉得有必要先解释一下处理器,寄存器,高速缓存,内存之间的关系:

    处理器大家都非常了解,就是负责做逻辑运算的,也就是你写的程序代码最终都会变成一条条指令或计算公式,这些玩意呢到处理器里面就变成了二进制的各种组合,处理器计算后会得到一个结果。

     

    寄存器是离处理器最近的一块存储介质,可以说位于内存模型的顶端,它的速度非常之快,快到可以和处理器相媲美,处理器从里面拿数据,运算完之后又把数据存回去。寄存器是处理器里面的一部分,处理器可能有多个寄存器,比如数据计数器,指令指针寄存器等等。

     

    高速缓存是一个比内存速度快很多接近处理器速度的存储区域,目的是把处理器要用到的一堆数据从主内存中复制进来供处理器享用,处理器享用完了之后又把结果同步回主内存,这样处理器只做自己的事,而高速缓存就成了传话筒。高速缓存有分为一级缓存,二级缓存和三级缓存,离处理器最近的是一级缓存,依次往后排,他们的容量大小也是由一往三 一级比一级大,所以你买电脑看CPU的时候可以关注下这些缓存,数值越大自然性能越好。

     

    主内存是我们通常讲的内存,比如现在的电脑动不动8G,16G啊等等,这些说的就是这个主内存,它比磁盘的读写速度快很多,但是又跟高速缓存没法比,因此,程序启动的时候,程序相关的数据会加载到主内存,然后处理器处理某块逻辑的时候,比较占空间的东西会丢到主内存,比如Java里面的对象,就是存放在堆上面的,而Java虚拟机里面的堆就是放在主内存的。

     

    那么问题来了,学挖掘机到底哪家强?(为啥处理器和内存之间要整的这么繁琐?直接主内存就用高速缓存代替不是简单粗暴高效吗?)

    答案很简单:土豪请随意!

     

    高速缓存的造价是非常高昂的,而内存成本要低很多,但是成本低带来的弊病也很明显,速度慢!

    其实饶了这么多,说白了就是费尽心思让你花着白菜价享受这法拉利的速度体验。

     

    用一个比较好懂的例子帮助大家记忆他们之间的关系:

    处理器好比皇帝,要处理每天的奏折,那么寄存器呢,就好比皇帝身边的太监,负责给皇帝递奏折,收奏折。皇帝身边可能好几个太监,比如专门递紧急事务奏折的太监甲,递一般性奏折的太监乙,递上供美女奏折的太监丙等,寄存器也分很多种,每种都是为了配合CPU进行高速计算和储存的。那么高速缓存呢,就好比朝中大臣,会收集各种重要信息,奏上去,最后这些奏折是通过太监递给皇上的。主内存呢,你可以理解成各地地方官,大臣收集他们的诉求,并反馈给他们。

     

    好了,啰嗦完了,我们现在回到开头讲的,基于高速缓存的存储交互可以很好的解决了处理器与内存之间的矛盾,但是也为计算机系统带来更高的复杂度,因为它引入了一个新的问题:缓存一致性。在多处理器系统中,每个处理器都拥有自己的高速缓存,而它们又共享同一个主内存,当多个处理器的运算任务都设计同一块主内存区域时,将可能导致各自的缓存数据不一致问题。这也是我们为什么要做同步的原因。

     

    Java内存模型

    了解了计算机的一个大致内存模型也很容易了解Java的内存模型。

    Java内存模型规定了所有的变量都存储在主内存中(Java中的主内存和刚才上面讲的计算机的主内存是一样的,只不过Java是占用分配给虚拟机的那一部分),每个线程还有自己的工作内存(可与前面讲的高速缓存类比),线程的工作内存中保存了被该线程使用到的变量的主内存副本拷贝(如果对象很大怎么办呢,比如10个MB?虚拟机肯定不会那么傻把对象直接拷贝进去啦,会拷贝对象的地址,以及需要用到的字段的值,哪怕值也很大,比如大的字符串,也会有相应的机制保证不会全拷贝,不然不是直接就爆掉了么)。

    线程对变量的所有操作(读取,赋值等)都必须在工作内存中进行,而不能直接读写主内存中的变量。本人之前有写过一篇博客《volatile的各种特性》,那么volatile可以保证任何情况下变量的可见性,是不是就是直接读主内存呢,事实上,volatile变量依然有工作内存的拷贝,但是它的操作顺序比较特殊,会每次都从主内存重新加载,所以你会看到每次volatile读取到的都是最新的值。

    不同的线程也无法访问其他线程的工作内存中的变量,线程间变量值的传递需要通过主内存来完成

    这里说的Java的主内存,工作内存与Java堆,栈,方法区等并不是同一个层次的划分,这两者基本是没有关系的,如果两者一定要勉强对应起来,那从变量,主内存,工作内存的定义上看,主内存只要对应于Java堆中的对象实例数据部分,工作内存对应于虚拟机栈中的部分区域。从更低层次上来说,主内存就是直接对应于物理硬件的内存,而为了获取更好的运行速度,虚拟机可能会让工作内存优先存储于寄存器和高速缓存区中,因为程序运行时主要访问读写的是工作内存。

     

    Java主内存与线程工作内存间8种交互操作

    Java一个变量如何从主内存拷贝到工作内存呢,又如何从工作内存同步回主内存呢?Java内存模型定义了8中操作来完成,虚拟机保证下面的每一种操作都是原子的。

     

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

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

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

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

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

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

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

    write(写入):作用于主内存变量,它把store操作中从工作内存得到的变量的值放入主内存的变量中。

     

    如果要把一个变量从主内存复制到工作内存,那就要顺序的执行read和load操作,如果要把工作内存的变量同步会主内存,那就要顺序的执行store和write操作。注意,Java内存模型只是要求上述两个操作必须按顺序执行,而没有保证是连续执行。也就是说,read与load之间,store与write之间是可以插入其他指令的。此外,Java内存模型还规定了在执行上述8中操作时必须满足如下规则:

    1)不允许read和load,store和write操作之一单独出现。即:不允许从主内存读取了,但工作内存不要它,或者从工作内存写出去的,主内存不要。

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

    3)不允许一个线程无原因地(没有进行任何assign操作的)把数据从线程的工作内存同步回主内存中

    4)一个新的变量必须在主内存中诞生,不允许在工作内存中直接使用一个未被初始化(load和assign)的变量

    5)一个变量在同一时刻只允许一条线程对其进行lock操作,但lock操作可以被同一条线程重复执行多次。

    6)如果对一个变量进行了lock操作,那将会清空工作内存中此变量的值,因此执行引擎使用这个变量之前,需要重新的进行load和assign操作。

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

    8)对一个变量执行unlock操作之前,必须先把此变量同步回主内存之中

     

    参考:

    《深入理解Java虚拟机》周志明著

    展开全文
  • 13.虚拟内存定义及实现方式

    千次阅读 多人点赞 2018-05-25 15:39:00
    传统存储管理方式的特征各种内存管理策略都是为了同时将多个进程保存在内存中以便允许多道程序设计。它们都具有以下两个共同的特征:1) 一次性作业必须一次性全部装入内存后,方能开始运行。这会导致两种情况发生:...
  • 简述计算机java内存

    千次阅读 2020-07-03 21:43:18
    当在一段代码块中定义一个变量时,java就在栈中为这个变量分配内存空间,当超过变量的作用域后,java会自动释放掉为该变量分配的内存空间,该内存空间可以立刻被另作他用。 堆内存:堆内存用于存储new出的对象和数组...
  • java数组定义、使用、以及数组内存分析详解

    千次阅读 多人点赞 2019-10-04 22:17:30
    文章目录1、什么是容器2、什么是数组3、数组的三种定义定义方式一定义方式二定义方式三数组定义格式详解:4、数组的访问5、什么是内存5.1 Java虚拟机的内存划分5.2 数组在内存中的存储5.2.1 一个数组内存图5.2.2两个...
  • Spark是基于内存计算的大数据并行计算框架。spark基于内存计算,提高了在大数据环境下数据处理的的实时性,同时保证了高容错性和高可伸缩性。  ---<<Spark大数据处理技术,应用与性能优化>.....
  • 现代计算机内存对齐机制

    万次阅读 2018-11-01 11:08:57
    64位计算机和32位计算机CPU对内存处理的区别 64位CPU,位宽为8个字节。(64位/8位/字节=8字节) 32位CPU,位宽位4个字节。(32位/8位/字节=4字节) 我们假想内存空间是一个二阶矩阵。(事实上内存是一维线性排列的)...
  • !...第一个我理解为 和 double对齐是16 第二个 是 4*3 = 12 第三个理解为 (1+7)+8= 16 第四个 8* 3 =24; 和运行的出来的结果完全不对啊,,看了好几篇 对齐的文章,,把文章示例抄过来运行都是错的。...
  • c++指针(二)——定义变量时的内存分配

    千次阅读 多人点赞 2018-05-08 08:37:02
    为什么要讲内存分配呢,因为要理解指针的实质,就...当我们定义一个int类型的变量 a 时,系统就开始准备一定的内存空间,这个内存空间的大小是int类型数据所需的内存大小(这个大小和具体的编译器有关,现在的编译器基...
  •  我们提出的弹性分布式数据集(RDDs),是一个让程序员在大型集群上以容错的方式执行基于内存计算的分布式内存抽象。RDDs受启发于两类使用当前计算框架处理不高效的应用:迭代算法和交互式数据挖掘工具。这二者在...
  • 定义不分配内存,变量定义分配内存。 <br /> 2。宏名和参数的括号间不能有空格 <br /> 3。宏替换只作替换,不做计算,不做表达式求解 <br /> //下面是正确的标准的写法 typedef int(FUNC...
  • 全面理解Java内存模型

    万次阅读 多人点赞 2016-09-21 18:39:21
    JMM定义了Java 虚拟机(JVM)在计算机内存(RAM)中的工作方式。JVM是整个计算机虚拟模型,所以JMM是隶属于JVM的。如果我们要想深入了解Java并发编程,就要先理解好Java内存模型。Java内存模型定义了多线程之间共享变量...
  • 如何计算一个结构体所占内存空间大小
  • 这几天有人问结构体占用内存的情况,
  • 定义不分配内存 计算圆的周长和面积 代码 //计算圆的周长和面积 #include<iostream> #define PI 3.14 //使用宏定义表示圆周率 using namespace std; void main() { int x; cout << "请输入圆的...
  • Java对象内存结构及大小计算

    千次阅读 2016-11-21 14:21:54
    本篇文章将想你介绍对象在内存中布局以及如何计算对象大小。 内存结构 在HotSpot虚拟机中,对象在内存中存储的布局可以分为3块区域:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。如下图所...
  • 本文使用三个例题带你五分钟搞懂DUP内存分配计算 基础概念解释 定义数据的时候会遇到如下几种指令,开始计算我们需要搞懂它们都代表多少字节: 定义字节数: 关键字 缩写 别名 定义的字节数 define byte ...
  • 如何计算Java对象所占内存的大小

    万次阅读 多人点赞 2018-05-24 11:42:12
    摘要:本文以如何计算Java对象占用内存大小为切入点,在讨论计算Java对象占用堆内存大小的方法的基础上,详细讨论了Java对象头格式并结合JDK源码对对象头中的协议字段做了介绍,涉及内存模型、锁原理、分代GC、OOP-...
  • 如何计算c++中开数组中占的内存

    千次阅读 2019-02-26 18:19:50
    首先要知道你自己开数组是的类型是不一样的内存的(也就是说类型不一样,内存计算的方法就不一样) 16位编译器 char :1个字节 char*(即指针变量): 2个字节 bool :1个字节 short int : 2个字节 int: 2个字节 ...
  • C语言中结构体大小的计算内存对齐详解)

    千次阅读 多人点赞 2018-03-24 17:13:04
    我们先来计算一下这个结构体的大小,如果不存在内存对齐这个问题,按理说这个结构体应该占(1+4+1)6个字节;然而事实上它占了12个字节,为什么?我们需要解决下面几个问题。 1.为什么存在内存对齐 平台...
  • C/C++--类占用内存的大小计算

    千次阅读 2015-03-24 19:51:44
    一个类占用多少内存,看下面代码: // TestVS2012.cpp : 定义控制台应用程序的入口点。 // #include "stdafx.h" #include using namespace std; int _tmain(int argc, _TCHAR* argv[]) { class Class1{}; ...
  • 具体的理论内容 神马是内存对齐以及为何要...这里对如何计算结构体所占内存大小做出总结 1、首先要弄清成员变量有效对齐方式的值 N 这里面有四个概念值: 1)数据类型自身的对齐值:就是上面交代的基本数据类型的自
  • 内存映射mmap是Linux内核的一个重要机制,它和虚拟内存管理以及文件IO都有...内核定义了mm_struct结构来表示一个用户进程的虚拟内存地址空间。 1. start_code, end_code指定了进程的代码段的边界,start_data, end_dat
  • sizeof计算对象所占内存的大小详解

    万次阅读 2016-11-17 22:23:49
    sizeof 怎样计算整型数组、字符数组、二维数组、类(是否为菱形继承、是否为虚继承等各种情况)的大小,非常详细的解释!!!
  • 软考——内存管理之页面调度与缺页计算

    千次阅读 热门讨论 2014-04-22 15:57:20
     说白点:缺页定义为所有内存块最初都是空的,所以第一次用到的页面都产生一次缺页,也就是内存中不存在的话,就会产生缺页。明白了这个之后,对于缺页计算问题就好说了。 二、先进先出的缺页计算问题  这种调度...
  • 结构体所占内存的字节数如何计算

    千次阅读 多人点赞 2019-08-22 21:43:23
    我们知道,为了提高内存寻址的效率,很多处理器体系结构为特定的数据类型引入 了特殊的内存对齐需求。不同的系统和编译器,内存对齐的方式有所不同,为了满足 处理器的对齐要求,可能会在较小的成员后加入补位,...
  • Linux 空闲内存和空闲CPU计算

    千次阅读 2010-12-14 11:31:00
     最近试验要统计linux系统中的空闲资源(主要是内存和CPU资源)。空闲内存比较容易直接一个free函数就好!但是空闲cpu就比较麻烦了,不像是windows环境下有很多工具和函数可以使用。通过不停的搜索终于找到了求...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 770,729
精华内容 308,291
关键字:

内存计算定义