Java内存模型 vs Java运行时数据区
- 两者没有任何关系,经过把前者认为是后者。
- Java内存模型是描述多线程程序的语义和规则,它指定规则,JVM来实现。
- Java内存模型主要是为了解决多线程中的可见性问题(写了读不到)。
- Java运行时数据区是JVM虚拟机的规范。
一个可见性的栗子
梳理流程:
主线程去写isRunning,子线程去读isRunning。是否打印i是子线程读取isRunning结果决定的,根据上图会不会是由于高速缓存的问题导致的呢?
- 流程:主线程改变变量isRunning -> 主线程写入高速缓存 -> 同步到RAM主内存 -> 同步到子线程的高速缓存 -> 子线程读取。
- 可见性问题:只要是写了但是读不到就是可见性问题。
- 高速缓存会导致可见性问题,但这个场景不是高速缓存导致的问题。因为有CPU的缓存同步协议。
揭晓答案:
这是由于while中触发了JIT编译器的执行,导致isRunning就算被修改了没用,不会再去读取isRunning的值。
JIT编译器(Just In Time Compiler)
- JAVA既有编译执行,又有解释执行。
- 先编译成二进制字节码文件 -> 加载到JVM方法区(加载的过程类似解释性语言一行行解释)。
- 当频繁调用,疯狂循环,JVM会由解释执行编程JIT编译执行。
- 之前isRunning都会去堆内存 -> 高速缓存里面去读,被JIT编译后则不会。
- 疯狂循环让JIT把isRunning缓存到机器码中(可以理解成动态及时编译机器码,把原先的机器码都改变了)不再去读取堆内存。
- 解决办法:加valatile关键字。
volatile关键字
Java内存模型规定:对volatile变量v的写入,与所有其他线程后续对v的读同步。即线程每次获取volatile变量的值都是最新的。保证了原子性。
- Java内存模型规定所有的变量都是存在主存当中,每个线程都有自己的工作内存。线程对变量的所有操作都必须在工作内存中进行,而不能直接对主存进行操作。并且每个线程不能访问其他线程的工作内存。变量的值何时从线程的工作内存写回主存,无法确定。所以出现了volatile。
- JAVA内存模型负责规定,由JVM去实现。
- 被编译成了ACC_VOLATILE,JAVA规范有定义。
- volatile会通知JIT编译器不要对其进行机器码缓存(机器码动态修改),必须查询堆内存。
- volatile的实现是基于CPU的指令重排。
Java内存模型内容细节
Shared Variables定义
Java内存模型规范这些被多个线程访问的共享变量如何巴拉巴拉。
线程间操作的定义
线程间操作:1、对共享变量的操作。2、一个线程对object加锁,其他线程拿不到这个锁受影响。3、线程第一个和最后一个操作。4、外部操作,如:一个线程操作数据库,另外的线程也会受影响。
Java内存模型还要规范线程间操作,保证可见性。
对于同步的规则定义
线程2拿到锁以后对线程1来说是可见的,线程1不能缓存。
因为有了JAVA内存模型,所以才能看到美好的世界(初始化默认值,其实默认值是乱码的,在内存中申请杂乱无章)。
线程被创建后,会在堆内存创建一个线程对象,也会创建一个线程独占空间的工作区域。
Happens-before规则定义
as-if-serial和happends-before的区别:
- as-if-serial是单CPU中指令重排的遵循规则。
- happends-before是多个线程之间的可见性规则(JMM规则)。
final在JMM中的处理
案例:
-
final修饰的字段可以看到正确的构建版本(即能读到正确的初始化值,比如上图的x=3,而没有被final修饰的字段其他线程可能会读到非初始化的值)。
-
x为final,而x又赋值给了y,那么y也是立即对其他线程可见。(解释文档中第二条)。