JVM内存模型和java内存模型(JMM)

面试怎么回答

JMM

怎么保证原子性、一致性、有序性

  • 主内存与工作内存
  • volatile
  • happens-before

线程安全的实现

  • 互斥同步 - synchronized 和 ReentrantLock
  • 非阻塞同步 - CAS

Brief Summary

复习到volatile的时候遇到了java memory model(JMM),提到了主内存和工作内存。之前看过JVM内存模型,不是很清楚这一个个都内存模型的,是要干什么。

JVM内存模型(区域)描述的是JVM对所管理的物理内存到底是怎么划分,即某个区域是用来干A事情,另一个区域是用来干B事情的。因为程序的运行需要记录很多数据,有程序中每个线程自己要记录的东西,也有整个程序中所有线程一起共享使用的数据。
拿现实中的例子就是,一个商店有很多工作人员,职责有相同的有不同的,作为店长要记录一些数据用来维持整个商店的运行,每个员工自己也要明白自己要干什么。所以就有需要一套规则用来记录前面提到的信息,不能东记一句西记一句吧,这个规则就是规定要怎么在大本本上有条理的记录信息。
这里拿1.8之前的JVM来说,因为1.8的还没看,应该只是元空间这个地方有不同。

所以就搞清楚啦,JVM内存模型是对物理内存功能上的划分。

Java内存模型是一个抽象概念,是对程序/线程对各种变量访问的规范。首先要知道为什么要对线程对变量的访问指定规则。
前面已经知道了,商店运行的信息都记在一个大本本上面,而且每个店内的人员都有机会看这个本子。但是一个很明显的规定就是,每个人能看到的内容是不一样的,是有权限的。比如,只有店长可以看所有人的工资。
那么在一个线程的程序里面,就我一个,我可以看所有的信息(而且只用一个本子),没有问题(单线程,顺序执行)。加入程序是多线程并行的,每个人都要一个个看的话(只有一个本子),效率就会很慢。
那么就可以多搞几个本子,那么问题就来了,几个员工看自己的本子,比方说记录自己的营业额,那么总营业额也要修改一下,但是所有人都一个本子,就不知道其他人有没有增加营业额了。那么就会出现问题。
映射在多线程并发的场景下,就会出现诸如原子性、可见性和有序性的问题。
那个JMM就是要给出线程要怎么使用内存或通过内存进行通信,使得程序运行的结果是正确的
我们要这些的规定/概念的原因(objective)就是为了保证程序的正确运行,进一步的,提高程序运行效率。

不要强迫自己把JVM内存模型和JMM联系起来

JVM内存模型概括

这里介绍1.8之前的JVM内存模型。

  • 程序计数器

记录的是当前线程正在执行的那一条字节码指令的地址。如果当前线程正在执行本地方法,则此时的程序计数器为0.随着线程的创建而创建,结束而结束。

  • java虚拟机栈

描述java方法运行的内存模型。每个线程都有各自的java虚拟机栈。

每一个即将运行的java方法创建一块叫做“栈帧”的区域。每个区域存储该方法运行过程中所需要的一些信息:局部变量表、操作数栈、动态链接、方法出口信息等。
在方法运行时,创建的局部变量会插入到局部变量表中。方法执行完毕后,栈帧就会出栈,释放内存空间。

栈内存大小可以设定为固定或者动态可变的。当设置为固定时,方法申请的栈帧容量大小超过这个设定的大小–>StackOverflowError。
当大小为动态可变时,方法申请栈最后耗尽所有内存–>OutOfMemoryError

  • 本地方法栈

与上一个类似,是调用本地方法的内存模型。

存放对象(类的实例)的内存空间。
由所有线程共享,在jvm启动时创建。
划分为:新生代(Eden,from survivor, to survivor)和老年代。GC的主要场所。
堆得大小也是可以固定可以扩展的。

  • 方法区(包含在堆内,放在老年代,因为需要长时间存在)

方法区用于存储JVM加载的类信息、final常量、static静态变量等数据,
方法区中的数据都是整个程序中唯一的。
方法区还包含了运行时常量池,
主要存放编译期生成的字面量和符号引用(在类加载后放入)。
String对象的字面量就会被放入到运行时常量池中。

JMM

前面已经说了,JMM给出了一些规则/约定,来保证多线程并行执行时程序结果正确。

具体说是要保证原子性、可见性和有序性。

首先对于每个线程,只能访问工作内存(每个员工有自己的可以知道的内容,也只有自己知道)。
主内存相当于大本本,保存着所有信息。工作内存需要用的、甚至是某个线程创建的本地变量也要放在主内存中。而工作内存保存的都是副本。

接下来就要规定一些规则,使得这个结构能够保证原子性、可见性和有序性。

  • 基本数据类型操作是原子性的(除long和double)

  • synchronized 和 Lock 保证代码块/方法的原子性操作

  • volatile保证可见性,不保证原子性

  • happens-before原则:

程序顺序原则,即在一个线程内必须保证语义串行性,也就是说按照代码顺序执行。

锁规则 解锁(unlock)操作必然发生在后续的同一个锁的加锁(lock)之前,也就是说,如果对于一个锁解锁后,再加锁,那么加锁的动作必须在解锁动作之后(同一个锁)。

volatile规则 volatile变量的写,先发生于读,这保证了volatile变量的可见性,简单的理解就是,volatile变量在每次被线程访问时,都强迫从主内存中读该变量的值,而当该变量发生变化时,又会强迫将最新的值刷新到主内存,任何时刻,不同的线程总是能够看到该变量的最新值。

线程启动规则 线程的start()方法先于它的每一个动作,即如果线程A在执行线程B的start方法之前修改了共享变量的值,那么当线程B执行start方法时,线程A对共享变量的修改对线程B可见

传递性 A先于B ,B先于C 那么A必然先于C

线程终止规则 线程的所有操作先于线程的终结,Thread.join()方法的作用是等待当前执行的线程终止。假设在线程B终止之前,修改了共享变量,线程A从线程B的join方法成功返回后,线程B对共享变量的修改将对线程A可见。

线程中断规则 对线程 interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过Thread.interrupted()方法检测线程是否中断。

对象终结规则 对象的构造函数执行,结束先于finalize()方法