什么是 jmm?
jmm,java内存模型;为了屏蔽不同系统及硬件的差异,java 虚拟机规范定义的线程并发时内存访问模型;
jmm从内存分配上,将内存虚拟的分为每个线程独有的工作线程,与共有的主线程;每个线程之间不能访问别的线程中的内存;从内存交互上定义了8种原子的 lock、unlock、read、load、use、assign、store、write 内存交互操作;
从内存访问安全特性上说;
1、原子性:对于简单的基本类型的赋值操作 比如 int i = 10,是原子操作;而对于 i = j ,则不是原子操作;同时使用 lock、unlock 来保证代码片段的原子性;java 关键字 synchronized 就是隐式的使用 lock/unlock 操作,synchronized 保证只能由一个线程访问 synchronized 锁住的代码,从而保证了原子性;
2、可见性:一个线程中对内存的修改操作,对别的线程可见;java 提供了 volatile 关键字实现了该功能;java 要求被 volatile 修饰的变量,在更新后立即同步到主内存,及对 volatile 修饰的变量 进行 assign 操作后应同时执行 store 与 write 操作,使得别的线程中对改变量的缓存数据失效;java 中的 synchronized 也可以实现可见性,是因为 java虚拟机 要求在执行 unlock 操作前需要执行 store 与 write 操作,将工作内存中的值同步到主内存中去;java 中被 final 修饰的实例变量,且在构造方法返回前不存在 this引用逃逸(多线程环境下,在对象构造方法返回之前,this引用被其余线程访问到)的时候,也是对其他线程可见的;java 实现 final实例变量的 可见性是在构造方法返回前添加 storestore 内存屏障;
int i ;
final int j;
static volatile A a;
private A(){
// a = this; 这会导致 this 引用逃逸
i = 1;
j = 2;
}
public static write(){
if(a == null){
a = new A()
}
}
pubic static read(){
if(a != null ){
int x = a.j;
int y = a.i;
}
}
线程1 执行 write 时,在执行构造函数时,可能会将 i 重排序到构造函数之外;线程2执行时,会读取到非预期的i = 0 ;
3、有序性:乱序的原因是重排序,编译器与cpu在编译与执行指令时,为了提高cpu的运行效率,会优化指令的执行的顺序;在单线程时,这种优化是安全的,但是在多线程交替执行时,这种优化会导致部分代码没有达到预期的值;java的hapen-before的8个规则,能够保证的代码的执行先后顺序;其中volatile变量规则,java内存模型通过使用内存屏障进行保证;jmm 描述了4种内存屏障,分别是 loadload,storestore,loadstore,store;以最保守的策略保证 volatile 的语义,java虚拟机对 volatile 写指令之前,添加 storestore 指令,在写之后添加 storeload指令,前者保证常规的写操作在 volatile 写之前完成,后者保证 volatile 写指令优先与其后的读指令完成;
对 volatile 字段读之后,先条件一个 loadload 指令,保证在 volatile 读在之后的读之前完成,再添加一个 loadstore 指令,保证之后的写指令务必在 volatile 读取完毕后才执行;***(内存屏障知识是虚拟机需要面对不同的处理器进行的封装,具体在什么处理器上面对什么语境插入怎样的屏障,我觉得没有必要深入了解,尽管我尝试理解了很久,20个小时吧,也才得到浅显的认识,但是现在我认为这是没有必要的)***
以 jdk1.5之后的 dcl 为例
private static volatile A singleton;
private A(){}
public static A getSingleton(){
if(singleton == null){
synchronized(A.class){
if(singleton == null){
singleton = new A()
return singleton ;
}
}
}
return singleton ;
}
在 jdk1.5之前,这一段代码是不安全的,因为之前的版本 volatile 字段不能保证其前后的代码的禁止重排序;常规情况下, singleton = new A()
可以分为 1、分配一个内存 2、执行构造方法 3、将内存地址赋值给引用 singleton ;
从 singleton 的写分析,如果可以重排序,则有可能执行 1 3 2,在执行 3 之后,线程切换到 判断 singleton != null;如果加上屏障,这里我们假设 3 是真实的 volatile 写指令,会在 3之前加上 storestore,保证2与1都优先与3执行;加上 storeload 指令,又保证了3执行后对所有的线程可见;(以上分析可能存在错误,纯属臆想,如有大佬愿意告诉我是错误的,我会看到后修改)