JVM学习笔记 05 - JMM简述

Posted 飞鸟还巢

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JVM学习笔记 05 - JMM简述相关的知识,希望对你有一定的参考价值。

JVM 试图定义一种统一的内存模型,能将各种底层硬件,以及操作系统的内存访问差异进行封装,使 Java 程序在不同硬件及操作系统上都能达到相同的并发效果。

JMM 的结构

JMM 分为主存储器(Main Memory)和工作存储器(Working Memory)两种。

  • 主存储器是实例位置所在的区域,所有的实例都存在于主存储器内。比如,实例所拥有的字段即位于主存储器内,主存储器是所有的线程所共享的。
  • 工作存储器是线程所拥有的作业区,每个线程都有其专用的工作存储器。工作存储器存有主存储器中必要部分的拷贝,称之为工作拷贝(Working Copy)。

在这个模型中,线程无法对主存储器直接进行操作。如下图,线程 A 想要和线程 B 通信,只能通过主存进行交换。

那这些内存区域都是在哪存储的呢?如果非要有个对应的话,你可以认为主存中的内容是 Java 堆中的对象,而工作内存对应的是虚拟机栈中的内容。但实际上,主内存也可能存在于高速缓存,或者 CPU 的寄存器上;工作内存也可能存在于硬件内存中,我们不用太纠结具体的存储位置。

8 个 Action(原子操作类型)

为了支持 JMM,Java 定义了 8 种原子操作(Action),用来控制主存与工作内存之间的交互。

  • (1)read(读取)作用于主内存,它把变量从主内存传动到线程的工作内存中,供后面的 load 动作使用。
  • (2)load(载入)作用于工作内存,它把 read 操作的值放入到工作内存中的变量副本中。
  • (3)store(存储)作用于工作内存,它把工作内存中的一个变量传送给主内存中,以备随后的 write 操作使用。
  • (4)write (写入)作用于主内存,它把 store 传送值放到主内存中的变量中。
  • (5)use(使用)作用于工作内存,它把工作内存中的值传递给执行引擎,每当虚拟机遇到一个需要使用这个变量的指令时,将会执行这个动作。
  • (6)assign(赋值)作用于工作内存,它把从执行引擎获取的值赋值给工作内存中的变量,每当虚拟机遇到一个给变量赋值的指令时,执行该操作。
  • (7)lock(锁定)作用于主内存,把变量标记为线程独占状态。
  • (8)unlock(解锁)作用于主内存,它将释放独占状态。


如上图所示,把一个变量从主内存复制到工作内存,就要顺序执行 read 和 load;而把变量从工作内存同步回主内存,就要顺序执行 store 和 write 操作。

三大特征

  • (1)原子性
    JMM 保证了 read、load、assign、use、store 和 write 六个操作具有原子性,可以认为除了 long 和 double 类型以外,对其他基本数据类型所对应的内存单元的访问读写都是原子的。
    如果想要一个颗粒度更大的原子性保证,就可以使用 lock 和 unlock 这两个操作。
  • (2)可见性
    可见性是指当一个线程修改了共享变量的值,其他线程也能立即感知到这种变化。
    我们从前面的图中可以看到,要保证这种效果,需要经历多次操作。一个线程对变量的修改,需要先同步给主内存,赶在另外一个线程的读取之前刷新变量值。
    volatile、synchronized、final 和锁,都是保证可见性的方式。
    这里要着重提一下 volatile,因为它的特点最显著。使用了 volatile 关键字的变量,每当变量的值有变动时,都会把更改立即同步到主内存中;而如果某个线程想要使用这个变量,则先要从主存中刷新到工作内存上,这样就确保了变量的可见性。
    而锁和同步关键字就比较好理解一些,它是把更多个操作强制转化为原子化的过程。由于只有一把锁,变量的可见性就更容易保证。
    (3)有序性
    Java 程序很有意思,从上面的 add 操作可以看出,如果在线程中观察,则所有的操作都是有序的;而如果在另一个线程中观察,则所有的操作都是无序的。
    除了多线程这种无序性的观测,无序的产生还来源于指令重排。
    指令重排序是 JVM 为了优化指令,来提高程序运行效率的,在不影响单线程程序执行结果的前提下,按照一定的规则进行指令优化。在某些情况下,这种优化会带来一些执行的逻辑问题,在并发执行的情况下,按照不同的逻辑会得到不同的结果。
    我们可以看一下 Java 语言中默认的一些“有序”行为,也就是先行发生(happens-before)原则,这些可能在写代码的时候没有感知,因为它是一种默认行为。
    先行发生是一个非常重要的概念,如果操作 A 先行发生于操作 B,那么操作 A 产生的影响能够被操作 B 感知到。
    下面的原则是《Java 并发编程实践》这本书中对一些法则的描述。
  • 程序次序:一个线程内,按照代码顺序,写在前面的操作先行发生于写在后面的操作。
  • 监视器锁定:unLock 操作先行发生于后面对同一个锁的 lock 操作。
  • volatile:对一个变量的写操作先行发生于后面对这个变量的读操作。
  • 传递规则:如果操作 A 先行发生于操作 B,而操作 B 又先行发生于操作 C,则可以得出操作 A 先行发生于操作 C。
  • 线程启动:对线程 start() 的操作先行发生于线程内的任何操作。
  • 线程中断:对线程 interrupt() 的调用先行发生于线程代码中检测到中断事件的发生,可以通过 Thread.interrupted() 方法检测是否发生中断。
  • 线程终结规则:线程中的所有操作先行发生于检测到线程终止,可以通过 Thread.join()、Thread.isAlive() 的返回值检测线程是否已经终止。
  • 对象终结规则:一个对象的初始化完成先行发生于它的 finalize() 方法的开始。

转载自:第18讲:大厂面试题:不要搞混 JMM 与 JVM · 深入浅出Java虚拟机 · 看云 

JAVA内存模型(JMM简述)

Java内存模型(Java Memory Model)就是一种符合内存模型规范的,屏蔽了各种硬件和操作系统的访问差异的,保证了Java程序在各种平台下对内存的访问都能保证效果一致的机制及规范

JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内用于存储线程私有的数据,而Java内存模型中规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可以访问,但线程对变量的操作(读取赋值等)必须在工作内存中进行
首先要将变量从主内存拷贝的自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写回主内存,工作内存和主内存 传值过程由JMM控制。
在JMM中主内存属于共享数据区域,从某个程度上讲应该包括了堆和方法区,而工作内存数据线程私有数据区域
从某个程度上讲则应该包括程序计数器、虚拟机栈以及本地方法栈。我们可能会看见主内存被描述为堆内存,工作内存被称为线程栈,实际上他们表达的都是同一个含义
主内存主要存储的是Java实例对象,所有线程创建的实例对象都存放在主内存中,不管该实例对象是成员变量还是方法中的本地变量(也称局部变量),当然也包括了共享的类信息、常量、静态变量。由于是共享数据区域,多条线程对同一个变量进行访问可能会发现线程安全问题。
工作内存
主要存储当前方法的所有本地变量信息(工作内存中存储着主内存中的变量副本拷贝),每个线程只能访问自己的工作内存,即线程中的本地变量对其它线程是不可见的,线程间无法相互访问工作内存,因此存储在工作内存的数据是线程安全的
存储在主内存不管它是基本数据类型或者包装类型(Integer、Double等)还是引用类型,都会被存储到堆区
JMM
线程 - 》cpu->cpu寄存器->cpu缓存-》主内存
executor->n线程->n内核线程->线程调度器-》cpu
Java内存模型和计算机硬件内存架构是一个相互交叉的关系,是一种抽象概念划分与真实物理硬件的交叉

指令重排
计算机在执行程序时,为了提高性能,编译器和处理器的常常会对指令做重排,一般分以下3种
1、编译器优化的重排
编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。
2、指令并行的重排
现代处理器采用了指令级并行技术来将多条指令重叠执行。如果不存在数据依赖性(即后一个执行的语句无需依赖前面执行的语句的结果),处理器可以改变语句对应的机器指令的执行顺序
3、内存系统的重排
由于处理器使用缓存和读写缓存冲区,这使得加载(load)和存储(store)操作看上去可能是在乱序执行,因为三级缓存的存在,导致内存与缓存的数据同步存在时间差。

其中编译器优化的重排属于编译期重排,指令并行的重排和内存系统的重排属于处理器重排,在多线程环境中,这些重排优化可能会导致程序出现内存可见性问题

如执行ADD指令时 写到内存时没有完成 后面计算就无法进行,停顿会造成CPU性能下降,因此我们应该想办法消除这些停顿,这时就需要使用到指令重排了,既然ADD指令需要等待,那我们就利用等待的时间做些别的事情。

A线程调用写入方法,而B线程调用读取方法
指令重排只会保证单线程中串行语义的执行的一致性,但并不会关心多线程间的语义一致性

在并发编程模式中,势必会遇到上面三个概念
1、可见性
可见性指的是当一个线程修改了某个共享变量的值,其他线程是否能够马上得知这个修改的值
对于串行程序来说,可见性是不存在的在多线程环境中线程A修改了共享变量x的值,还未写回主内存时,另外一个线程B又对主内存中共享变量x进行操作。
2、有序性
有序性是指对于单线程的执行代码,我们总是认为代码的执行是按顺序依次执行的,但对于多线程环境,则可能出现乱序现象,因为程序编译成机器码指令后可能会出现指令重排现象,重排后的指令与原指令的顺序未必一致多线程环境下,一个线程中观察另外一个线程,所有操作都是无序的
3、原子性
一个操作或者多个操作要么全部执行要么全部不
执行。
JMM提供的解决方案:
理解了原子性,可见性以及有序性问题后,看看JMM是如何保证的,对于方法级别或者代码块级别的原子性操作,可以使用synchronized关键字或者重入锁(ReentrantLock)保证程序执行的原子性
而工作内存与主内存同步延迟现象导致的可见性问题,可以使用synchronized关键字或者volatile关键字解决,它们都可以使一个线程修改后的变量立即对其他线程可见 对于指令重排导致的可见性问题和有序性问题,则可以利用volatile关键字解决,因为volatile的另外一个作用就是禁止重排序优化


仅靠sychronized和volatile关键字来保证原子性、可见性以及有序性,那么编写并发程序可能会显得十分麻烦
volatile在并发编程中很常见
1、当一个线程修改了一个被volatile修饰共享变量的值,新值总数可以被其他线程立即得知
2、禁止指令重排序优化
volatile禁止重排优化:
内存屏障,又称内存栅栏,是一个CPU指令,它的作用有两个,一是保证特定操作的执行顺序,
二是保证某些变量的内存可见性(利用该特性实现volatile的内存可见性)。由于编译器和处理器都能执行指令重排优化。
如果在指令间插入一条Memory Barrier则会告诉编译器和CPU,不管什么指令都不能和这条Memory Barrier指令重排序,
Memory Barrier的另外一个作用是强制刷出各种CPU的缓存数据,因此任何CPU上的线程都能读取到这些数据的最新版本

以上是关于JVM学习笔记 05 - JMM简述的主要内容,如果未能解决你的问题,请参考以下文章

JVM_12 JMM内存模型

JVM_12 JMM内存模型

JAVA内存模型(JMM简述)

03 JVM 从入门到实战 | 简述垃圾回收算法

重新认识 Java 内存模型(JMM)

深入理解JVM虚拟机读书笔记——内存模型与线程