一、JMM模型:Java内存模型(Java Memory Model)是一种抽象的概念,描述的是一组规则/规范,定义了程序中各变量的访问方式。JVM运行程序的实体是线程。JMM时围绕原子性、有序性、可见性展开。
1、线程创建时JVM会为其创建一个工作内存(栈空间),线程只能操作工作内存,不能直接操作主内存中的变量,工作内存中存储着主内存变量的副本拷贝;
2、所有变量都存储在主内存(共享内存区域),线程不能操作主内存的变量,线程间通信(传值)必须通过主内存来完成;
主内存:主要存储Java实例对象,所有线程创建的实例对象都存放在主内存中。
工作内存:
主要存储当前方法所有本地变量信息(主内存变量副本拷贝),包括字节码行号指示器、Native方法信息;每个线程只能访问自己的工作内存,即线程中的本地变量对其他线程使不可见的,因此存储在工作内存中的数据不存在线程安全问题。
根据JVM虚拟机规范,对于一个实例对象中的成员方法而言,如果方法包含本地变量时基本数据类型,将直接存储在工作内存的栈帧结构中;如果本地变量时引用类型,则该变量的引用会存储在栈帧中,对象实例存储在主内存中。主内存中的实例对象可以被多线程共享,多个线程同时调用同一个对象的同一个方法,那么每个线程将要操作的数据拷贝一份到各自的工作内存,执行完操作后再刷到主内存。
Java内存模型与硬件内存架构的关系:
多线程的执行最终都会映射到硬件处理器上进行执行,但Java内存模型和硬件内存架构并不完全一致。对于硬件内存来说只有寄存器、缓存内存、主内存的概念,并没有工作内存和主内存之分。JMM只是一种抽象的概念,是一组规则,并不实际存在,不管工作内存的数据还是主内存的数据,对于计算机硬件来说都会存储在计算机主内存中,当然也有可能存储到CPU缓存或者寄存器中,因此总体上来说,Java内存模型和计算机硬件内存架构师一个相互交叉的关系,是一种抽象概念划分与真实物理硬件的交叉。
JMM存在的必要性
如果存在两个线程同时对一个主内存中的实例对象的变量进行操作就有可能诱发线程安全问题。
以上关于主内存与工作内存之间的具体交互协议,即一个变量如何从主内存拷贝到工作内存、如果从工作内存同步到主内存之间的实现细节,Java内存模型定义了以下八种操作来完成;
数据同步八大原子操作
1、lock(锁定):作用于主内存变量,把一个变量标记为一条线程独占状态;
2、unlock(解锁):作用于主内存变量,把一个处于锁定状态的变量释放出来,释放后才能被其他线程锁定;
3、read(读取):作用于主内存变量,把一个变量值从主内存传输到线程的工作内存,以便后续load载入;
4、load(载入):作用于工作内存变量,把read操作从主内存中得到的变量值放入工作内存的变量副本中;
5、use(使用):作用于工作内存变量,把工作内存中的一个变量值传递给执行引擎;
6、assign(赋值):作用于工作内存变量,把一个从执行引擎接收到的值赋给工作内存的变量
7、store(存储):作用于工作内存变量,把工作内存中的一个变量的值传送到主内存中,以便随后的write的操作;
8、write(写入)作用于工作内存变量,把store操作从工作内存中的一个变量的值传送到主内存的变量中;
如果要把一个变量从主内存中复制到工作内存中,就需要按顺序地执行read和load操作,如果把变量从工作内存中同步到主内存中,就需要按顺序地执行store和write操作。
同步规则:
1、不允许一个线程无原因地(没有发生过任何assign操作)把数据从工作内存同步回主内存中;
1、一个新的变量只能在主内存中诞生,不予许在工作内存中直接使用一个未被初始化(load和assign)的变量。
2、一个变量在同一时刻只允许一条线程对其进行lock操作,但lock操作可以被同一线程重复执行多次,只有执行相同次数的unlock后,变量才会被解锁,必须成对出现lock、unlock。
3、如果对一个变量执行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量之前需要重新执行load或assign操作初始化变量。
4、如果一个变量没有被lock锁定,则不允许对它执行unlock操作,也不允许去unlock一个被其他线程锁定的变量。
5、对一个变量执行unlock操作之前,必须先把此变量同步到主内存中(执行store和write操作)
原子性&可见性&有序性
原子性:一个操作不可中断,一旦开始就不会被其他线程影响;
可见性:当一个线程修改了某共享变量的值,其他线程是否能够立马感知;指令重排、编译器优化都有可能导致可见性问题,工作内存与主内存同步延迟现象造成了可见性问题。
有序性:指令重排现象、工作内存与主内存同步延迟现象,导致一个线程观察另一个线程,所有操作都是无序的。
JMM如何解决原子性&可见性&有序性
原子性问题:
除了JVM自身提供的对基本数据类型读写操作的原子性外,还可以通过synchronized和Lock实现原子性。因为synchronized和Lock能够保证任一时刻只有一个线程访问该代码块。
可见性问题:
volatile关键字保证可见性。当一个共享变量被volatile修饰时,它会保证修改的值立即被其他的线程看到,即修改的值立即更新到主内存中,当其他线程需要读取时,它会去内存中读取新值。
synchronized和Lock也可以保证可见性,因为它们可以保证任一时刻只有一个线程能访问共享资源,并在其释放锁之前将修改的变量刷新到主内存中。
有序性问题:
volatile可以保证一定的有序性,synchronized和Lock也可以保证有序性
指令重排:Java语言规范规定JVM线程内部维持顺序化语义。即只要程序的最终结果与它顺序化情况的结果相等,那么指令的执行顺序可以与代码顺序不一致,此过程叫指令的重排序。
意义:JVM能根据处理器特性(CPU多级缓存、多核)适当的对机器指令进行重排序,使机器指令能更符合CPU的执行特性,最大限度的发挥机器性能。
as-if-serai语义:不管怎么重排序,程序的执行结果不能改变。编译器、runtime和处理器都必须遵守as-if-serial语义。
happens-before原则:从JDK5开始,使用新的JSR-133内存模型,提供了happens-before原则来辅助保证程序执行的原子性、可见性和有序性。
二、volatile内存语义
volatile是Java虚拟机提供的轻量级的同步机制,volatitle关键字有如下两个作用
1、保证被volatile修饰的共享变量对所有线程总是可见的。新值对其他线程立即感知
2、禁止指令重排序优化
volatile禁止重排优化
硬件的内存屏障:intel硬件提供了一系列的内存屏障,主要有:
1、lfence,读屏障
2、sfence,写屏障
3、mfence,全能型屏障
4、Lock前缀
不同硬件实现内存屏障的方式不同,Java内存模型屏蔽了这种底层硬件平台的差异,由JVM来为不同的平台生成相应的机器码,提供了四类内存屏障指令LoadLoad、StoreStore、LoadStore、StoreLoad。
内存屏障,又称内存栅栏,是一个CPU指令,它的作用有两个,一是保证特定操作的执行顺序,二是保证某些变量的内存可见性。由于编译器和处理器都能执行指令重排优化。如果在指令间插入一条Memory Barrier则会告诉编译器和CPU,不管什么指令都不能和这条Memory Barrir指令重排序,即通过插入内存屏障禁止在内存屏障前后的指令执行重排序优化。