Java内存模型--JMM
Java内存模型 (JMM)和JVM运行时内存的区别
JVM运行时内存
Java运行时内存模型,描述了Java程序代码在运行时,一次执行单个语句或者表达式时(即通过单个线程执行时)不同类型的变量、引用、对象、类等等的一些信息的存储规范
。
Java内存模型
描述了多个线程运行时的语义规范
,比如多个线程修改了共享内存的值时,应该读取到哪个值得规则。
下边我们来看一个多线程读取的一个问题代码
/**
* 提供一个产生多线程可见性的原因代码
* 理论上在子线程在主线程将flag改为false之后应该打印出i的值,但实际运行中没有
*/
public class MultiThreadProblem {
public static boolean flag = true;
//public volatile static boolean flag = true; //这样就可以保证flag变量的可见性
public static void main(String[] args) throws InterruptedException {
//开启一个子线程来根据flag的值进行i++操作
new Thread(()->{
int i = 0;
System.out.println(Thread.currentThread().getName() + "正在运行," + "flag=" + flag);
while (flag){
i++;
}
System.out.println("flag=false,i=" + i);
}).start();
//主线程睡眠3s后改变flag的值
Thread.sleep(3000L);
flag =false;
System.out.println("主线程更改了flag的值为:" + flag);
}
}
上边问题引发的原因是,两个线程都从内存共享区的方法区复制了一份flag变量的值true放到各自的虚拟机栈中,虽然主线程将更改了flag=false,并且通过也将更改的值覆盖了方法区的值,但是子线程在执行while语句的时候,CPU因为while的时间操作较长而将while语句进行了指令重排。CPU指令重排在单线程中是没有问题的,但是在多线程中操作一个共享变量,指令重排就可能会对结果产生影响。
因为Java语言是介于脚本语言和编译语言之间,当解释器读到while语句的时候,JIT编译器遇到while循环和反复调用的情况,就会将当前的代码进行指令重排。然后这个指令重排,对于多线程来说,如果其他线程对于共享变量有更改的话,就可能产生可见性问题,就像上边代码中将flag改为false后,子线程不会感知的到。
使用了volatile关键字之后,就会在编译的时候告诉JVM有关该变量的操作不进行指令重排,一旦有线程更改了变量之后,就会将该变量写入内存中。
这些关于多线程中共享内存(或者堆内存)中的共享变量冲突问题,就是JMM约束的范围,它描述线程间操作的语义规范。
JMM规定有以下几个:
-
对于同步的规则定义参考图片
-
happens-before先行发生原则,参考图片
-
被final定义的对象,对于所有线程都会看到正确的构造版本。通常被static final修饰的字段不能被修改。然而System.in/System.out/System.err被static final修饰,却可以被修改,这是个遗留问题。必须通过set方法改变,我们将这些字段称为写保护,以区别于普通final字段。
-
Word Tearing(字分裂)字节处理,参考图片
-
double 和long的特殊处理
这两个都是64位的,它的单次写操作都是分两次来进行的,每次操作其中32位的时候,可能导致第一次写入后,读取的值是脏数据,第二次写完成后,才能读到正确数据。所以建议用volatile来修饰double和long才行。