简述Java内存模型

Java内存模型即Java Memory Model,简称JMM,其规范了Java虚拟机与计算机内存时如何协同工作的,规定了一个线程如何和何时看到其他线程修改过的值,以及在必须时,如何同步访问共享变量。

JVM的内存分配

在解释Java内存模型之前,我们先了解下JVM的内存分配的几个概念,如下图所示,Java内存模型把内存分为两大块,一个是堆一个是栈。

  • 堆heap:运行时的数据区,由垃圾回收负责,动态分配大小。存取速度较慢;
  • 栈stack:存取速度比堆快,仅次于寄存器,数据可以共享,大小和生存期等是固定的。

Java内存模型要求调用栈和本地变量存放在线程栈上,对象存放在堆上。
一个变量也可能是指向一个对象的引用,引用这个变量是放在线程栈上,但对象本身是放在堆上的。
一个对象它可能包含方法(methodOne..),方法包含本地变量(Local variable1..),这些本地变量都仍然是放在线程栈上的,即使这些方法所属的对象存放在堆上,一个对象的成员变量可能会随着对象自身存放在堆上,不管这个对象是原始类型还是引用类型。
静态成员变量跟随着类的定义存放在堆上,存放在堆上的对象可以被所持有对这个对象引用的线程访问。
当一个线程可以访问一个对象时,它也可以访问这个对象的成员变量,如果两个线程同时调用同一个对象上的同一个方法,他们都会访问这个对象的成员变量,但是每一个线程都拥有了成员变量的私有拷贝。

计算机硬件架构

接下来我们再来看看计算机硬件架构的图示:

这里是个多CPU的结构,一个cpu中可能还包含多核。因此我们可以看出,在有两个或者多个cpu的现代计算机上,同时运行多个线程是有可能的,而且每个cpu在某个时刻运行一个线程是没问题的。若Java程序是多线程的,在Java程序中,每个cpu上一个线程是可能同时并发执行的。

在CPU内部有一组CPU寄存器,也就是CPU的储存器。

CPU操作寄存器的速度要比操作计算机主存快的多,在主存和CPU寄存器之间还存在一个CPU缓存,CPU操作CPU缓存的速度快于主存但慢于CPU寄存器。即CPU操作的速度上主存 < 缓存 < 寄存器。某些CPU可能有多个缓存层(一级缓存和二级缓存)。计算机的主存也称作RAM,所有的CPU都能够访问主存,而且主存比上面提到的缓存和寄存器大很多。

当一个CPU需要访问主存时,会先读取一部分主存数据到CPU缓存,进而在读取CPU缓存到寄存器。当CPU需要写数据到主存时,同样会先将寄存器的数据刷新到CPU缓存,然后再在某些节点把缓存数据刷新到主存。

Java内存模型和硬件内存架构并不一致。硬件内存架构中并没有区分栈和堆,从硬件上看,所有的线程栈和堆都分布在主主存中,当然一部分栈和堆的数据也有可能会存到CPU缓存和寄存器中,如下图所示,Java内存模型和计算机硬件内存架构是一个交叉关系:

并发编程模型的两个关键问题

在并发编程中,需要处理两个关键问题:线程之间如何通信及线程之间如何同步(这里的线程是指并发执行的活动实体)。通信是指线程之间以何种机制来交换信息。在命令式编程中,线程之间的通信机制有两种:共享内存和消息传递。

在共享内存的并发模型里,线程之间共享程序的公共状态,通过写-读内存中的公共状态进行隐式通信。在消息传递的并发模型里,线程之间没有公共状态,线程之间必须通过发送消息来显式进行通信。
同步是指程序中用于控制不同线程间操作发生相对顺序的机制。在共享内存并发模型里,同步是显式进行的。程序员必须显式指定某个方法或某段代码需要在线程之间互斥执行。
在消息传递的并发模型里,由于消息的发送必须在消息的接收之前,因此同步是隐式进行的。
Java的并发采用的是共享内存模型,Java线程之间的通信总是隐式进行,整个通信过程对程序员完全透明。

Java内存模型的抽象结构

接下来我们从抽象角度看看线程和主存之间的抽象关系:

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

从上图来看,如果线程A与线程B之间要通信的话,必须要经历下面2个步骤。
1)线程A把本地内存A中更新过的共享变量刷新到主内存中去。
2)线程B到主内存中去读取线程A之前已更新过的共享变量。

本地内存A和本地内存B由主内存中共享变量x的副本。假设初始时,这3个内存中的x值都为0。线程A在执行时,把更新后的x值(假设值为1)临时存放在自己的本地内存A中。当线程A和线程B需要通信时,线程A首先会把自己本地内存中修改后的x值刷新到主内
存中,此时主内存中的x值变为了1。随后,线程B到主内存中去读取线程A更新后的x值,此时线程B的本地内存的x值也变为了1。
从整体来看,这两个步骤实质上是线程A在向线程B发送消息,而且这个通信过程必须要经过主内存。JMM通过控制主内存与每个线程的本地内存之间的交互,来为Java程序员提供内存可见性保证。

Java内存模型的同步操作和规则

为了保证并发时程序处理的准确性,这里就需要一些同步的手段,这里我们介绍一下Java内存模型定义的同步的八种操作和一些规则。

八种操作

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

规则

  1. 不允许read/load,store/write单一出现,且必须按顺序执行,但中间可以插入其他指令;
  2. 不允许一个线程丢弃离他最近的assign操作
  3. 不允许一个线程未发生assign操作就将数据同步至主线程;
  4. 一个新的变量,只能从主内存中诞生,不允许在工作内存中生成一个未被初始化的变量。
  5. 一个变量在同一时刻只允许一个线程执行lock操作,lock可以被同一个线程执行多次,需要相同次数的unlock操作才能解锁;
  6. 如果一个变量执行了lock操作,则会清空工作内存中的值,执行引擎使用这个变量前需要重新执行load或者assign操作来拿到初始化变量的值;
  7. 如果一个变量没有被lock操作执行,则不允许对其进行unlock操作,也不允许unlock一个被其他线程lock的变量,unlock操作执行之前,必须将此变量同步回主内存。

参考文档:
1. 《Java并发编程的艺术》
2. https://blog.csdn.net/suifeng3051/article/details/52611310
3. https://coding.imooc.com/learn/list/195.html

点赞

发表评论

%d 博主赞过: