深入理解Java虚拟机(五)Java内存模型

文章目录

      • 1. 主内存与工作内存
      • 2. 内存间交互操作
      • 3. Java内存模型的三大特性
      • 4. JMM中的happens-before原则(先行发生原则)

注意:Java内存模型和Java运行时数据区域是属于不同层次的概念,请不要混淆。
  Java虚拟机中定义了一种内存模型(即为Java Memory Model,简称JMM)。Java内存模型用来屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的内存访问效果。在此之前,C/C++直接使用物理硬件和操作系统的内存模型,因此,会由于不同平台下的的内存模型的差异,有可能导致程序在一套平台上并发完全正常,而在另一套平台上并发访问经常出错。

1. 主内存与工作内存

  Java内存模型本身是一种抽象的概念,并不真实存在,它描述的是一种规则或规范,通过这组规范定义了程序中各个变量(包括实例域、静态域和构成数组对象的元素)的访问方),即在JVM中将变量存储到内存和从内存中取出变量这样的细节。此处变量包括实例字段、静态字段和构成数组对象的元素, 但不包括局部变量和方法参数,因为后两者时线程私有的,不会被共享。
  因为JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存,用于存储线程私有的数据,而Java内存模型中规定所有变量都存储在主内存主内存时共享内存区域,所有线程都可以访问但线程对变量的操作(读取赋值等)必须在工作内存中进行,而不能直接读写主内存中的变量。 那么就要先进行下列的步骤:

  • 将变量从主内存拷贝到线程的工作内存空间;
  • 执行程序,对变量进行操作(读写赋值等);
  • 操作完成后,将变量写回主内存。

  工作内存中存储着主内存的变量副本拷贝,工作内存是线程私有的数据区域,所以不同线程之间无法访问对象的工作内存,线程通信(变量值的传递)必须通过主内存来完成。
线程、主内存、工作内存三者之间的交互关系如下图:
深入理解Java虚拟机(五)Java内存模型_第1张图片
  上面我们说过JMM与Java内存区域不是同一层次的概念,但是我们可能在初学习中经常会搞混,它们肯定会有一定的相似性,下来我们就来说说它们的相似点。JMM与Java内存区域唯一相似点,都存在共享数据区域和私有数据区域,在JMM中主内存属于共享数据区域,从某个程度上讲应该包括了堆和方法区,而工作内存数据线程私有数据区域,从某个程度上讲则应该包括程序计数器、虚拟机栈以及本地方法栈。或许在某些地方,我们可能会看见主内存被描述为堆内存,工作内存被称为线程栈,实际上他们表达的都是同一个含义。

  • 主内存
    主要存储的是Java实例对象,所有线程创建的实例对象都存放在主内存中,不管该实例对象是成员变量还是方法中的本地变量(也称局部变量),除此之外还包含共享的类信息、常量、静态变量。由于是共享数据区域,多条线程对同一变量进行访问可能会发生线程安全问题。
  • 工作内存
    主要存储的是当前方法的所有本地变量信息(工作内存中存储着主内存中的变量副本拷贝),每个线程只能访问自己的工作内存,即线程中的本地变量对其他线程是不可见的,就算两个线程执行的是同一段代码,它们也会各自在自己的工作内存中创建属于当前线程的本地变量,由于是线程私有数据区域,也应该包括程序计数器(字节码行号指示器)、虚拟机栈 以及 本地方法栈(Native方法)。
    注意: 由于工作内存是每个线程的私有数据区域,线程间无法相互访问工作内存,因此存储在工作内存中的数据不存在线程安全问题。

2. 内存间交互操作

  关于主内存与工作内存之间的具体交互协议,即一个变量如何从主内存中拷贝到工作内存如何从工作内存同步回主内存之类的实现细节,Java内存模型中定义了下面8种操作来完成。JVM实现是必须保证下面提及的每一种操作的原子性、不可再分性。

  • lock(锁定):作用于主内存的变量,它把变量标识为一条线程独占的状态。
  • unlock(解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
  • read(读取):作用于主内存的变量,它把一个变量的值从主内存传输到线程的工作内存,以便随后的load动作使用。
  • load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中。
  • use(使用):作用于工作内存的变量,它把工作内存中的一个变量的值传递给执行引擎。、
  • assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋给工作内存的变量。
  • store(存储):作用于工作内存的变量,它把工作内存中的一个变量的值传送到主内存中,以便后续write操作使用。
  • write(写入):作用于主内存的变量,它把store操作从工作内存中得到的变量的值放到主内存的变量中。

3. Java内存模型的三大特性

  由于JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存(有些地方称为栈空间),用于存储线程私有的数据,线程与主内存中的变量操作必须通过工作内存间接完成,主要过程是将变量从主内存拷贝的每个线程各自的工作内存空间,然后对变量进行操作,操作完成后再将变量写回主内存,如果存在两个线程同时对一个主内存中的实例对象的变量进行操作就有可能诱发线程安全问题。
举例:主内存中存在一个共享变量x,现在有A和B两条线程分别对该变量x进行操作,A/B线程各自的工作内存中存在共享变量的副本x。
 假设A线程现在要修改x的值,B线程却想要读取x的值,那么B线程读取到的值时A线程更新后的值2还是更新前的值1呢?
 答案是:不确定,即B线程有可能读到A线程更新前的值,也有可能读取到A线程更新后的值。这是因为工作内存是每个线程私有的数据区域,而线程A修改变量x的时候,首先将变量从主内存拷贝到A线程的工作内存中,然后对变量进行操作,操作完成后再将变量x写回主内存中;而对于B线程的操作也是类似的,这要就有可能造成主内存与工作内存间存在一致性问题。
 假如A线程修改完后正在将数据写回主内存,而B线程此时正在读取主内存,即将x拷贝到自己的工作内存中,这样B线程读取到的x值就是A更新前的值,但如果A线程已将更新后的x写回主内存后,B线程才开始读取的话,那么此时B线程读取到的x就是A更新后的值,但到底是哪种情况先发生呢?这是不确定的,这也就是所谓的线程安全问题。
深入理解Java虚拟机(五)Java内存模型_第2张图片为了解决类似上面的线程安全问题,JVM定义了一组规则,通过这组规则来决定一个线程对共享变量的写入何时对另一个线程课件,这组规则也称为Java内存模型(即JMM),JMM围绕着程序执行的原子性、有序性、可见性展开的。

  • 原子性: 指一个操作是不可中断的,要么不执行,要么执行且在执行过程中不会被任何因素打断。由Java内存模型来直接保证的原子性变量操作包括:read、load、assign、use、store和read。大致可以认为,基本数据类型的访问读写是具备原子性的(64位虚拟机)。如果需要大范围的原子性,就需要synchronized关键字约束。
  • 可见性: 指当一个线程修改了共享变量的值,其他线程就能立即知道这个修改操作。volatile、synchronized、final三个关键字可以实现可见性。
  • 有序性: 指对于单线程的执行的代码,总是认为代码的执行是按顺序依次执行的,但在线程中观察另外一个线程,所有的操作都是无序的。前半句是指“线程内表现为串行”,后半句是指“指令重排序”和“工作内存与主内存同步延迟现象”。

4. JMM中的happens-before原则(先行发生原则)

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

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

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

你可能感兴趣的:(Java,Java虚拟机,Java内存模型,Java)