JVM:Java内存模型

 2019-12-10 16:05  阅读(804)
文章分类:Java Core

简述

Java内存模型(Java Memory Model,JMM),是用来屏蔽掉各种硬件和操作系统的内存访问差异,以实现Java程序在各种 平台下都能达到一致的内存访问效果.

在JVM内部,Java内存模型把内存分成了两部分:线程栈区和堆区,下图展示了Java内存模型在JVM中的逻辑视图:
20191210001548\_1.png

JVM中运行的每个线程都拥有自己的线程栈,线程栈包含了当前线程执行的方法调用相关信息,我们也把它称作调用栈。随着代码的不断执行,调用栈会不断变化。

线程栈还包含了当前方法的所有本地变量信息。一个线程只能读取自己的线程栈,也就是说,线程中的本地变量对其它线程是不可见的。即使两个线程执行的是同一段代码,它们也会各自在自己的线程栈中创建本地变量,因此,每个线程中的本地变量都会有自己的版本。

所有原始类型(boolean,byte,short,char,int,long,float,double)的本地变量都直接保存在线程栈当中,对于它们的值各个线程之间都是独立的。对于原始类型的本地变量,一个线程可以传递一个副本给另一个线程,但它们之间是无法共享的。

堆区包含了Java应用创建的所有对象信息,不管对象是哪个线程创建的,其中的对象包括原始类型的封装类(如Byte、Integer、Long等等)。不管对象是属于一个成员变量还是方法中的本地变量,它都会被存储在堆区。

下图展示了调用栈和本地变量都存储在栈区,对象都存储在堆区:
20191210001548\_2.png

变量存储位置

1)一个本地变量如果是原始类型,那么它会被完全存储到栈区。

2)一个本地变量如果是一个对象的引用,引用变量会被存储到栈中,对象本身仍然存储在堆区。

3)一个对象的成员方法中包含的本地变量,仍需要存储在栈区,即使它们所属的对象在堆区。

4)一个对象的成员变量,不管它是原始类型还是包装类型,都会被存储到堆区。

5)Static类型的变量以及类本身相关信息都会随着类本身存储在堆区。

6)堆中的对象可以被多线程共享:

如果一个线程获得一个对象的引用,它便可访问这个对象的成员变量。

如果两个线程同时调用了同一个对象的同一个方法,那么这两个线程便可同时访问这个对象的成员变量,但是对于本地变量,每个线程都会拷贝一份到自己的线程栈中。
20191210001548\_3.png

主内存和工作内存

Java内存模型的主要目标是定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量这样的底层细节。此处的变量(Variables)与java编程中的变量是有所区别的,它包括实例字段、静态字段和构成数组对象的元素,但不包括局部变量与方法参数。

Java内存模型规定了所有的变量都存储在主内存(Main Memory)中,每条线程还有自己的工作内存(Working Memory),线程的工作内存中保存了被该线程使用到的变量,是从主内存拷贝的副本。线程对内存的所有操作(读取、赋值等)都必须在工作内存中进行,而不能直接操作主内存,不同线程之间也无法直接访问对方工作内存中的变量,线程间变量值得传递都需要通过主内存来完成,线程、主内存、工作内存之间的关系如图所示:
20191210001548\_4.png

内存间交互

关于主内存与工作内存之间具体的交互协议,即一个变量如何从主内存拷贝到工作内存、如何从工作内存同步回主内存之类的细节,Java内存模型定义了以下8种操作来完成,它们都是原子操作(除了对long和double类型的变量)。

  • 1)lock(锁定)
    作用于主内存中的变量,它将一个变量标志为一个线程独占的状态。
  • 2)unlock(解锁)
    作用于主内存中的变量,解除变量的锁定状态,被解除锁定状态的变量才能被其他线程锁定。
  • 3)read(读取)
    作用于主内存中的变量,它把一个变量的值从主内存中传递到工作内存,以便进行下一步的load操作。
  • 4)load(载入)
    作用于工作内存中的变量,它把read操作传递来的变量值放到工作内存中的变量副本中。
  • 5)use(使用)
    作用于工作内存中的变量,这个操作把变量副本中的值传递给执行引擎。当执行需要使用到变量值的字节码指令的时候就会执行这个操作。
  • 6)assign(赋值)
    作用于工作内存中的变量,接收执行引擎传递过来的值,将其赋给工作内存中的变量。当执行赋值的字节码指令的时候就会执行这个操作。
  • 7)store(存储)
    作用于工作内存中的变量,它把工作内存中的值传递到主内存中来,以便进行下一步write操作。
  • 8)write(写入)
    作用于主内存中的变量,它把store传递过来的值放到主内存的变量中。

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

  • 1.不允许read和load、store和write操作之一单独出现。即不允许一个变量从主内存被读取了,但是工作内存不接受,或者从工作内存回写了但是主内存不接受。
  • 2.不允许一个线程丢弃它最近的一个assign操作,即变量在工作内存被更改后必须同步改更改回主内存。
  • 3.工作内存中的变量在没有执行过assign操作时,不允许无意义的同步回主内存。
  • 4.在执行use前必须已执行load,在执行store前必须已执行assign。
  • 5.一个变量在同一时刻只允许一个线程对其执行lock操作,一个线程可以对同一个变量执行多次lock,但必须执行相同次数的unlock操作才可解锁。
  • 6.一个线程在lock一个变量的时候,将会清空工作内存中的此变量的值,执行引擎在use前必须重新read和load。
  • 7.线程不允许unlock其他线程的lock操作。并且unlock操作必须是在本线程的lock操作之后。
  • 8.在执行unlock之前,必须首先执行了store和write操作。

特殊规则

对于volatile型变量的特殊原则

volatile变量的两种特性

内存可见性

可见性意指当一个线程修改了主内存中的变量的值,这个新值对其他线程而言是立即可以得知的。普通变量不能做到这点。

禁止指令重排序

普通变量紧紧能保证在方法的执行过程中,所有依赖变量赋值后结果的地方能取到正确的值(当然多线程环境可能某个方法被多个线程同时执行而有违此点),而不能保证变量赋值的操作顺序与程序代码的执行顺序一致。

volatile变量的特殊规则

定义变量V、W被volatile修饰,线程T会操作变量V和W。下面用浅显的语言解释Java内存模型对其的特殊规则:

  • 每次使用前从主内存读取
    read、load、use必须顺序整体出现。前一个操作是load时才能use,后一个操作时use时才能load。
  • 每次修改后立即同步回主内存
    assign、store、write必须顺序整体出现。前一个操作是assign时才能store,后一个操作时store时才能assign。
  • 避免指令重排序
    如果T对V的use或者assign先于T对W的use或者assign,那么T对V的load或者write必须先于T对W的load或者assign。

使用场景

根据volatile变量的特性,在不符合下面两个规则的情况下,仍旧需要通过加锁(隐式或者显式)来保证原子性。

  1. 运算结果不依赖变量的当前值,或者能够确保只有单一的线程修改变量的值

比如i++操作需要依赖变量的当前值进行计算,使用volatile修饰并不能保证原子性

  1. 变量不需要与其他状态变量共同参与不变的约束

    volatile int i=1,j=2; if(i<j){ //本线程读取到i、j的值时条件成立 Thread.sleep(5000);//其他线程修改了i、j的值,使得上面的if不成立,但是下面的代码依旧会执行,这不是我们想要的 doSomething(); }

对于long 和 double 型变量的特殊规则

对于64位的非volatile修饰的long和double型数据,Java内存模型允许虚拟机对其操作分为2次32位的操作来进行,允许Java虚拟机不保证read、load、store和write这四个操作的原子性。这就是long和double的非原子性协议。

因此,如果多个线程同时对未被volatile保护的变量进行读取和修改操作,可能某些线程读取到一个既不是原值,也不是被某个线程修改之后的值这样一个“半个变量”的数值。

不过这种“半个变量”非常罕见,Java内存模型只是规定虚拟机不需要把这4种操作实现为原子的,但是现代商用的大多数虚拟机都会选择将其实现为原子操作,因此我们编写代码时不用将多线程环境下的long和double变量特意声明为volatile

内存模型特征

Java内存模型就是围绕着在并发过程中如何处理原子性、可见性和有序性这三个特性来建立的。

1、原子性(Atomicity)

由Java内存模型类直接保证原子性变量的操作包括read、load、use、store和write。可以认为基本数据类型的访问读写是具备原子性的。另外需要注意的是非原子性协定,即:Java内存模型中定了一条相对宽松的规定,允许虚拟机将没有被volatile修饰的64位数据读写操作分为两次32位操作进行,也就是说,对long、double这种64位的数据类型的read、load、store和write操作可以不保证原子性操作。这种情况在64的虚拟机下是不会发生的,64位虚拟机下是原子性的,但是在32位的虚拟机下,这组操作不是原子性的。如果要更大范围的原子性保证,Java内存模型提供了lock和unlock操作,字节码指令对应的是monitorenter和monitorexit,反映到Java代码就是synchronized了。

2、可见性(Visibility)

可见性是指一个线程修改了共享变量的值,其他线程能立即得知这个修改。上边在讲volatile变量的时候讲过,Java内存模型是通过在变量修改后立即将最新的值同步到主内存、在变量读取前从主内存再次读取最新值来保证可见性的。volatile变量与普通变量的区别是,volatile变量的特殊规则保证了最新值能立刻同步到主内存。而普通变量不能保证这一点。除了volatile外,还有synchronized和final关键字能保证可见性。synchronized的可见性对应Java内存模型中lock和unlock,lock会清空工作内存中变量的副本,unlock操作前会将变量同步到主内存(前边有提到)。

3、有序性(ordering)

如果从本线程里观察,所有操作都是有序的。如果从一个线程观察另一个线程,所有的操作可能都是无序的。也就是说,线程内表现为串行语义,多个线程间指的是指令重排序和工作内存和主内存同步延迟的问题。

先行发生原则(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关系,并不意味前一个操作必须要在后一个操作之前执行!仅仅要求前一个操作的执行结果,对于后一个操作是可见的,且前一个操作按顺序排在后一个操作之前。

点赞(0)
版权归原创作者所有,任何形式转载请联系作者; Java 技术驿站 >> JVM:Java内存模型

相关推荐