JVM-Java内存模型

206 阅读7分钟

在上一篇文章中提到:
java之所以能跨平台,是因为jvm(java虚拟机)可以跨平台。
本章主要详细说说Java内存模型

JAVA内存模型

java虚拟机规范中试图定义一种java内存模型(Java Memory Model,JMM),用来屏蔽掉各种硬件和操作系统的内存访问差异,以实现让java程序在各种平台下都能达到一致的内存访问效果。

未命名绘图-jmm内存模型.drawio.png

java内存模型(JMM)描述了程序中变量的访问规则,以及将变量存储和读取的底层细节。所有的变量都存储在主内存中,每个线程的工作内存都相互独立,工作内存中保存了该线程使用到变量的副本(从主存中拷贝的一份)。

  • 线程对所有共享变量的操作都必须在自己的工作内存中进行,不能直接从主内存中获取。
  • 不同线程之间无法直接访问其它工作内存中的变量,线程变量值的传递需要通过主内存来完成。

重排序

在执行程序时,为了提高性能,程序在运行过程中,并不是一定按照我们编写的顺序进行执行。处理器和编译器常常会对指令进行重排序。重排序必须满足以下两个条件。

  • 单线程下,不能改变程序运行的结果
  • 存在数据依赖的情况下,不能重排序

重排序的概念

  • 编译器优化的重排
    编译器在不改变线程程序语义的前提下,可以重新安排语句的执行顺序。
  • 指令并行的重排
    现代处理器采用了指令级并行技术来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应的机器指令的执行顺序
  • 内存系统的重排
    由于处理器使用缓存和读写缓冲区,这使得加载和存储在操作上看上去可能是在乱序执行,由于三级缓存,导致内存与缓存的数据同步存在时间差

as-if-serial语义

as-if-serial语义指: 所有操作为了提升性能都可以进行重排序,但是必须保证重排序之后的结果不能改变。注意: as-if-serial语义只保证单线程环境,在多线程下无效。

int a = 1;
int b = 2;
int c = a + b;

a和b不存在依赖关系,c依赖a和b。在重排序的时候,可能是a-->b-->c;也有可能是b-->a-->c。无论以哪种方式,最后c的值一定是3。

原子性

原子性指的是一个操作是不可中断的,即使是在多线程环境下,一个操作一旦开始就不会被其他线程影响。比如对于一个静态变量int x,两条线程同时对他赋值,线程A赋值为1,而线程B赋值为2,不管线程如何运行,最终x的值要么是1,要么是2,线程A和线程B间的操作是没有干扰的,这就是原子性操作,不可被中断的特点。

可见性

可见性指的是当一个线程修改了某个共享变量的值,其他线程是否能够马上得知这个修改的值。
对于单线程来说,即使进行了重排序,也不存在可见性的问题,因为我们在任何一个操作中修改了某个变量的值,后续的操作中都能读取这个变量值,并且是修改过的新值。
但是对于多线程环境可就不一定了,由于线程对共享变量的操作都是线程拷贝到各自的工作内存进行操作后才写回到主内存中的,这就可能存在一个线程A修改了共享变量x的值,还未写回主内存时,另外一个线程B又对主内存中同一个共享变量x进行操作,但此时A线程工作内存中共享变量x对线程B来说并不可见,这种工作内存与主内存同步延迟现象就造成了可见性问题,另外指令重排以及编译器优化也可能导致可见性问题,通过前面的分析,我们知道无论是编译器优化还是处理器优化的重排现象,在多线程环境下,确实会导致程序轮序执行的问题,从而也就导致可见性问题。

有序性

有序性是指对于单线程的执行代码,我们总是认为代码的执行是按顺序依次执行的,这样的理解并没有毛病,毕竟对于单线程而言确实如此,但对于多线程环境,则可能出现乱序现象,因为程序编译成机器码指令后可能会出现指令重排现象,重排后的指令与原指令的顺序未必一致。在Java程序中,倘若在本线程内,所有操作都视为有序行为,如果是多线程环境下,一个线程中观察另外一个线程,所有操作都是无序的,前半句指的是单线程内保证串行语义执行的一致性,后半句则指指令重排现象和工作内存与主内存同步延迟现象。

happends-before原则

在Java中,提供了synchronizedvolatile关键字来保证原子性、可见性和有序性,并且volatile关键字还有禁止重排序的作用。当然,除了上面的两点,JMM内部还定义一套happens-before 原则来保证多线程环境下两个操作间的原子性、可见性以及有序性。

Java 内存模型在底层处理器内存模型的基础上,定义自己的多线程语义。它明确指定了一组排序规则,来保证线程间的可见性。这一组规则被称为 Happens-Before, JMM 规定,要想保证 B 操作能够看到 A 操作的结果(无论它们是否在同一个线程),那么 A 和 B 之间必须满足 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()方法

volatile

volatile是Java中的一个关键字,主要有以下两个作用:

  • 保证变量在多线程中可见,A线程修改了x变量值,新的值可以在其他线程中立即被获取。
  • 禁止指令重排序