什么是Java内存模型(JMM)?

什么是Java内存模型(JMM)?

    一、JMM的相关概念
    Java内存模型简称JMM(Java Memory Model),是Java虚拟机所定义的一种抽象规范,用来屏蔽不同硬件和操作系统的内存访问差异,让java程序在各种平台下都能达到一致的内存访问效果。
   这里要注意两点:

   1)JMM是一个抽象的概念,并不是物理上的内存划分。

   2)JMM-JVM-硬件的关系:Java内存模型(JMM)定义了Java虚拟机(JVM)在计算机内存(RAM)中的工作规范。在硬件内存模型中,各种CPU架构的实现是不尽相同的,Java作为跨平台的语言,为了屏蔽底层硬件差异,定义了Java内存模型(JMM)。JMM作用于JVM和底层硬件之间,屏蔽了下游不同硬件模型带来的差异,为上游开发者提供了统一的使用接口。
   总之一句话:JMM是JVM的内存使用规范,是一个抽象的概念。

   Java内存模型的抽象示意图:

 

    如上图在JMM中,内存划分为两个区域,主内存和工作内存。

    1.   主内存(Main Memory)

    主内存被所有的线程所共享,所有的变量都存储在主内存中,包括实例变量,静态变量,但是不包括局部变量和方法参数。

    2.  工作内存(Working Memory)

    每一个线程拥有自己的工作内存,线程的工作内存保存了该线程用到的变量和主内存的副本拷贝。

    说明:

    1) 每个线程的工作内存都是独立的,线程操作数据只能在工作内存中进行,然后刷回到主内存。这是 Java 内存模型定义的线程基本工作方式。

    2)线程对共享变量的所有操作都必须在工作内存进行,不能直接读写主内存中的变量。不同线程之间也无法访问彼此的工作内存,变量值的传递只能通过主内存来进行。

    二、JMM定义了什么?

    整个Java内存模型实际上是围绕着三个特征建立起来的。分别是:原子性,可见性,有序性。这三个特征可谓是整个Java并发的基础。

    1. 原子性

    原子性指的是一个操作是不可分割,不可中断的,一个线程在执行时不会被其他线程干扰。

    下面的这几句代码能保证原子性吗?我们一起来看下

1 int i = 2;
2 
3 int j = i;
4 
5 i++;
6 
7 i = i + 1;

   第一句是基本类型赋值操作,必定是原子性操作。

   第二句先读取i的值,再赋值到j,两步操作,不能保证原子性。

   第三和第四句其实是等效的,先读取i的值,再+1,最后赋值到i,三步操作了,不能保证原子性。

   实现方式:在 Java 中, JMM只能保证基本的原子性。如果要保证一个代码块的原子性,可以借助synchronized(提供了monitorenter 和 moniterexit 两个字节码指令)、各种 Lock 以及各种原子类实现原子性。synchronized 和各种 Lock 可以保证任一时刻只有一个线程访问该代码块,因此可以保障原子性。各种原子类是利用 CAS (compare and swap)

   2. 可见性

   可见性指当一个线程修改共享变量的值,其他线程能够立即知道被修改了。

   实现方式:在 Java 中,可以借助synchronized、volatile 以及各种 Lock 实现可见性。

   如果我们将变量声明为 volatile ,这就指示 JVM,这个变量是共享且不稳定的,每次使用它都到主内存中进行读取。
   synchronized的原理是,在执行完,进入unlock之前,必须将共享变量同步到主内存中。

   final修饰的字段,一旦初始化完成,如果没有对象逸出(指对象未初始化完成就可以被别的线程使用),那么对于其他线程都是可见的。

   3. 有序性

   有序性即程序执行的顺序按照代码的先后顺序执行。

   说明:由于指令重排序问题,代码的执行顺序未必就是编写代码时候的顺序。指令重排序可以保证串行语义一致,但是没有义务保证多线程间的语义也一致 ,所以在多线程下,指令重排序可能会导致一些问题。
   实现方式:在 Java 中,可以使用synchronized或者volatile保证多线程之间操作的有序性。

   volatile关键字是使用内存屏障达到禁止指令重排序,以保证有序性。

   synchronized的原理是,一个线程lock之后,必须unlock后,其他线程才可以重新lock,使得被synchronized包住的代码块在多线程之间是串行执行的。

   三、八种内存交互操作

   内存交互操作有8种,如下图:

  1. lock(锁定)

  作用于主内存的变量,把一个变量标识为线程独占的状态。

  2. read(读取)

  作用于主内存的变量,把一个变量值从主内存传输到线程的工作内存中,以便下一步的load操作使用。

  3. load(载入)

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

  4. use(使用)

  作用于工作内存中的变量,表示把工作内存中的一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时就会执行该操作。

  5. assign(赋值)

 作用于工作内存的变量,它把一个从执行引擎返回的结果赋值给工作内存的变量副本中,每当虚拟机遇到一个给变量赋值的字节码指令时将会执行这个操作。

  6. store(存储)

  作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中,以便后续的write的操作。

  7. wirte(写入)

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

  8. unlock(解锁) 

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

  说明:

  1)如果要把一个变量从主内存中复制到工作内存中,就需要按顺序地执行 read 和 load 操作。

  2)如果把变量从工作内存中同步到主内存中,就需要按顺序地执行 store 和 write 操作。

  但Java 内存模型只要求上述操作必须按顺序执行,而没有保证必须是连续执行。

  下面继续补充一下JMM对8种内存交互操作制定的规则:

  1)不允许read、load、store、write操作之一单独出现,也就是read操作后必须load,store操作后必须write。

  2)不允许线程丢弃他最近的assign操作,即工作内存中的变量数据改变了之后,必须告知主存。

  3)不允许线程将没有assign的数据从工作内存同步到主内存。

  4)一个新的变量必须在主内存中诞生,不允许工作内存直接使用一个未被初始化的变量。就是对变量实施use、store操作之前,必须经过load和assign操作。

  5)一个变量同一时间只能有一个线程对其进行lock操作。多次lock之后,必须执行相同次数unlock才可以解锁。

  6)如果对一个变量进行lock操作,会清空所有工作内存中此变量的值。在执行引擎使用这个变量前,必须重新load或assign操作初始化变量的值。

  7)如果一个变量没有被lock,就不能对其进行unlock操作。也不能unlock一个被其他线程锁住的变量。

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

   四、Java 内存结构和 JMM 有何区别?

  这是一个比较常见的问题,很多初学者非常容易搞混。

  Java 内存结构和内存模型是完全不一样的两个东西。

  1. Java内存结构

  Java 内存结构和 Java 虚拟机的运行时区域相关,定义了 JVM 在运行时如何分区存储程序数据,就比如说堆主要用于存放对象实例。

  2. Java内存模型(JMM)
  Java 内存模型和 Java 的并发编程相关,抽象了线程和主内存之间的关系就比如说线程之间的共享变量必须存储在主内存中,规定了从 Java 源代码到 CPU 可执行指令的这个转化过程要遵守哪些和并发相关的原则和规范,其主要目的是为了简化多线程编程,增强程序可移植性的。

 

   参考链接:

   https://zhuanlan.zhihu.com/p/258393139

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章