JMM(Java内存模型)
概述
- JMM即Java内存模型(Java Memory Model),是一种抽象的概念,并不真实存在,JMM描述的是一组规则或规范,通过这组规范定义了程序中各个变量的访问方式
- Java内存模型中规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可以访问,但线程对变量的操作必须在工作内存中进行,所以首先要将变量从主内存拷贝的自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写回主内存,不能直接操作主内存中的变量
- JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存,用于存储线程私有的数据
- 工作内存中存储着主内存中的变量副本拷贝,不同的线程间无法访问对方的工作内存,线程间的通信必须通过主内存来完成
JMM规范
JMM与JVM内存区域都存在共享数据区和私有数据区的概念
- 在JMM中主内存属于共享数据区,从某个程度上讲应该包括了堆和方法区
- 在JMM中工作内存属于线程私有数据区,从某个程度上讲则应该包括程序计数器、虚拟机栈以及本地方法栈
JMM与硬件内存架构的关系
从上图可以得出JMM和计算机硬件内存架构是一个相互交叉的关系,是一种抽象概念划分与真实物理硬件的交叉。最终多线程的执行最终都会映射到硬件处理器上进行执行,Java内存模型和硬件内存架构并不完全一致,JMM对内存的划分对硬件内存并没有任何影响,不管是工作内存的数据还是主内存的数据,对于计算机硬件来说,在计算机主内存、CPU缓存或者寄存器中,都可能存在
- 硬件内存:分为寄存器、CPU缓存、主内存
- JMM:分为工作内存(线程私有数据区域)和主内存(堆内存)
JMM作用
多个线程同时对一个主内存中的实例对象的变量进行操作有可能导致线程安全问题,所以需要JMM保证主内存与工作内存间数据一致
从上图可以得出如下信息
- CPU的运行并不是直接操作内存而是先把内存里边的数据读到缓存,而内存的读和写操作在并发场景下就会造成不一致的问题。所以JVM规范中定义一种Java内存模型(java Memory Model,简称JMM)来屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的内存访问效果;
- JMM规范了Java虚拟机与计算机内存是如何协同工作的:规定了一个线程如何和何时可以看到由其他线程修改过后的共享变量的值,以及在必要时如何同步的访问共享变量,实现了线程和主内存之间的抽象关系
JMM内存交互操作
JMM内存交互协议指的是主内存与工作内存之间的交互协议,JMM定义了八种内存交互操作来定义一个变量应该如何从主内存拷贝到工作内存、如何从工作内存同步到主内存。
主内存和工作内存同步变量值规则分析
- 不允许一个线程无原因地(没有发生过任何assign操作)把数据从工作内存同步回主内存中
- 一个新的变量只能在主内存中诞生,不允许在工作内存中直接使用一个未被初始化(load或者assign)的变量。即就是对一个变量实施use和store操作之前,必须先自行assign和load操作。
- 一个变量在同一时刻只允许一条线程对其进行lock操作,但lock操作可以被同一线程重复执行多次,多次执行lock后,只有执行相同次数的unlock操作,变量才会被解锁。lock和unlock必须成对出现。
- 如果对一个变量执行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量之前需要重新执行load或assign操作初始化变量的值。
- 如果一个变量事先没有被lock操作锁定,则不允许对它执行unlock操作;也不允许去unlock一个被其他线程锁定的变量。
- 对一个变量执行unlock操作之前,必须先把此变量同步到主内存中(执行store和write操作)
JMM如何解决可见性,有序性,原子性问题
在多线程场景下,由于多线程情况复杂,为了保证每个线程能看到正确的结果,所以必须要保证线程的可见性,有序性与原子性
- 可见性问题
- volatile关键字保证可见性。当一个共享变量被volatile修饰时,它会保证修改的值立即被其他的线程看到,即修改的值立即更新到主存中,当其他线程需要读取时,它会去内存中读取新值
- synchronized和Lock也可以保证可见性,因为它们可以保证任一时刻只有一个线程能访问共享资源,并在其释放锁之前将修改的变量刷新到内存中
- 有序性问题
- volatile关键字可以保证“一定程度上的有序性”,一定程度上禁止指令重排
- synchronized和Lock也可以保证“一定程度上的有序性”,不过与volitile的效果不同,synchronized和Lock保证每个时刻只有一个线程执行同步代码,相当于是让线程顺序执行同步代码,自然就保证了有序性
- 原子性问题
- JDK提供的对基本数据类型读写操作的原子性,如AtomicInteger类等
- synchronized和Lock实现原子性。因为synchronized和Lock能够保证任一时刻只有一个线程访问该代码块
重排序
概述
重排序是编译器和处理器为了优化程序性能而对指令序列进行重排序,从Java源代码到最终实际执行的指令序列,会分别经历下面3种重排序:
- 编译器重排序的典型就是通过调整指令顺序,在不改变程序语义的前提下,尽可能减少寄存器的读取、存储次数,降低了重复读取的开销,充分复用寄存器的存储值
- 指令集并行的重排序是对CPU的性能优化,通过重排尽可能阻止流水线技术中断,提升CPU执行性能
- 内存系统重排序可以通过伪重排序减少CPU与主内存交互时CPU的短暂卡顿,从而提升性能(但延时写入主内存可能会导致数据不一致)
数据依赖性
如果两个操作访问同一个变量,且这两个操作中有一个为写操作,此时这两个操作之间就存在数据依赖性,数据依赖性分为下列三种类型
上述三种情况下只要重排序两个操作的执行顺序,程序的执行结果就会改变。编译器和处理器在重排序时,会遵循数据依赖性,编译器和处理器不会改变存在数据依赖性关系的两个操作的执行顺序
as-if-serial语义
不管怎么重排序(编译器为了提高并行度),(单线程)程序的执行结果不能被改变.编译器、runtime和处理器都必须遵守as-if-serial语义
重排序原则
内存屏障
内存屏障是为了禁止编译器重排序和CPU重排序,内存屏障在编译器和CPU层面上都有对应的指令(其中CPU的内存屏障是CPU提供的指令,可以由开发者显示调用)
Happens-Before 规则
- as-if-serial语义保证单线程内程序的执行结果不会被改变,happens-before具有传递性关系,保证正确同步的多线程程序的执行结果不会被改变。且两者的目的都是为了在不改变程序执行结果的前提下,尽可能的提高程序执行的并行度
- 程序顺序规则:在一个线程内一段代码的执行结果是有序的(在单线程情况下,对不存在数据依赖性的指令进行重排序,只保证单线程执行结果的正确性,不保证程序在多线程中执行的正确性)
- 监视器锁规则:就是无论是在单线程环境还是多线程环境,对于同一个锁来说,一个线程对这个锁解锁之后,另一个线程获取了这个锁都能看到前一个线程的操作结果!(管程是一种通用的同步原语,synchronized就是管程的实现)
- volatile变量规则:就是如果一个线程先去写一个volatile变量,然后一个线程去读这个变量,那么这个写操作的结果一定对读的这个线程可见
- 线程启动规则:在主线程A执行过程中,启动子线程B,那么线程A在启动子线程B之前对共享变量的修改结果对线程B可见
- 线程终结规则:在主线程A执行过程中,子线程B终止,那么线程B在终止之前对共享变量的修改结果在线程A中可见
- 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生
- 对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始(finalize:垃圾回收机器(Garbage Collection),也叫GC)
- 传递性:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C