JMM 知识点总结
java 内存模型总结,主要方便查看
JMM
屏蔽各种硬件和操作系统内存方法差异,以实现让 Java 程序在各个平台下都能达到一致的内存方法效果。
主要的目的:定义程序中各种变量的访问规则( 关注在虚拟机中变量存取与内存交互的底层细节 )
包括:实例字段、静态字段和构成数组的对象元素,不包括局部变量与方法参数
除了增加高速缓存外,为了使处理器内部的运算单元能尽量被充分利用,处理器可能会对输入代码进行乱序执行优化,处理器会在计算之后将乱序执行的结果进行重组,保证该结果与顺序执行的结果一致。
但并不保证程序中各个语句计算的先后顺序与输入代码中顺序一致,因此如果存在一个计算任务依赖另一个计算任务的中间结果,那么其顺序性并不能靠代码的先后顺序保证。
与处理器乱序执行优化类似,JVM的即时编译中也有指令重排序优化。
volatile
volatile 保证可见性
Java线程内存模型确保所有线程看到这个变量的值是一致的
会多出 lock 前缀指令
基于 缓存一致性协议 来实现的
- 将当前处理器缓存行的数据写回到系统内存
最近的处理器里,lock 信号一般不锁总线而是锁缓存
通过缓存一致性协议来确保修改的原子性,它会阻止同时修改两个以上处理器缓存内存区域数据
2.这个写回内存的操作会使在其他 CPU 里缓存了该内存地址的数据无效 ( 通过缓存一致性协议 ( 处理器的嗅探技术 ) 来实现的 ( 阻止同时修改由两个以上处理器缓存的内存区域数据 ) )
volatile 的内存语义
- 可见性,对一个 volatile 变量的读,总是能看到任意线程对这个 volatile 变量最后的写入
2.原子性,对任意单个 volatile 变量的读/写具有原子性,但类似于 volatile ++ 这种复合操作不具有原子性
从内存语义的角度来说,volatile 写和锁的释放具有相同的内存语义, volatile 读与锁的获取具有相同的内存语义
volatile 写内存语义
当写一个 volatile 变量时,JMM会把该线程对应的本地内存中的共享变量值刷新到主内存中
volatile 读内存语义
当读一个 volatile 变量时,JMM会把该线程对应的本地内存置为无效。线程接下来将从主内存中读取共享变量
volatile 的内存语义实现
通过内存屏障来实现的
synchronized
Java 的每个对象都可以作为锁
对于普通同步方法,锁是当前实例对象
对于静态同步方法,锁是当前类的 Class 对象
对于同步方法块,锁是 Synchonized 括号里配置的对象
当一个线程试图访问同步代码块时,它首先必须得到锁,退出或抛出异常时必须释放锁
原理:
JVM基于进入和退出 Monitor( 管程 ) 对象来实现方法同步和代码块同步( monitorenter 和 monitorexit )
锁的内存语义
当线程释放锁时,JMM会把该线程对应的本地内存中的共享变量刷新到主内存中。
当线程获取锁时,JMM会把该线程对应的本地内存置为无效。从而保证使得监视器保护的临界区代码必须从主内存中读取共享变量
锁的释放与 volatile 写有相同的内存语义,锁获取与 volatile 读有相同的内存语义
原子操作
不可被中断的一个或一系列操作
处理器通过总线锁定和缓存锁定两个机制来保证复杂内存操作的原子性
- 循环 CAS 实现原子性 CMPXCHG 指令实现 没有变化则更新
CAS:1.ABA 2.循环时间长开销大 3.只能保证一个共享变量的原子操作
2. 使用锁实现原子性
除了偏向锁,JVM实现锁的方法都用了循环CAS,即当一个线程想进入同步块的时候,使用循环CAS来获取锁,当它退出同步块时使用循环CAS释放锁
共享变量:所有的实例域、静态域和数组元素
本地内存:抽象概念,涵盖了缓存、写缓冲区、寄存器以及其他的硬件和编译器优化
StoreLoad Barriers 是一个全能型的屏障,同时具有其他3个屏障的效果。执行该屏障的开销很昂贵,因为当前处理器通常要把写缓冲区中的数据全部刷新到内存中
在 JMM 中,如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须要存在 happens-before 关系。这里提到的两个操作既可以是一个线程内,也可以是不同线程之间
编译器和处理器在重排序时,会遵守数据依赖性,编译器和处理器不会改变存在数据依赖关系的两个操作的执行顺序,仅仅针对单个处理器和单个线程
重排序
编译器和处理器为了优化程序性能而对指令序列进行重新排序的一种手段
as-if-serial: 不管怎么重排序,单线程程序的执行结果不能被改变。编译器、runntime 和处理器都必须遵守的
as-if-serial 把单线程程序保护起来了,使得单线程程序看起来是按照顺序来执行的
从 jdk5 开始,JMM 只允许把一个 64 位 long/double 型变量的写操作拆分为两个 32 位的写操作来执行,任意的读操作都必须是原子性的
final域的内存语义
- 在构造函数内对一个 final 域写入,与随后把这个被构造对象的引用赋值给一个引用变量,这两个操作不能重排序
2.初次读一个包含 final 域的对象引用,与随后初次读这个 final 域,这个两个操作不能重排序
3.在构造函数内对一个 final 引用对象的写入,与随后在构造函数外这个被构造对象引用赋值给一个引用变量,不能重排序
在对象引用为任意线程可见之前,对象的 final 域已经被正确的初始化过了,而普通域不具有这个保障。
happens-before 提供跨线程的内存可见性保证
只要不改变程序的执行结果( 单线程程序和正确同步的多线程程序 ),编译器和处理器怎么优化都行。例如,如果编译器经过细致的分析后,认定一个 volatile 变量只会被单个线程访问,那么编译器可以把这个 volatile 变量当做一个普通变量来对待,如果编译器进行细致的分析后,认定一个 锁 只会被单个线程访问,那么这个锁可以清除。