Java内存模型之简要知识与规范梳理
- 2020 年 3 月 10 日
- 笔记
1. JMM简要知识
语义规范
- Java编程语言的语义允许编译器和微处理器执行优化,从而与不正确同步的代码进行交互来完成工作.
- 线程内语义是单线程程序的语义,它允许根据线程内读操作看到的值来完全预测线程的行为;由于单线程内的实现是在其上下文执行的, 可以通过评估线程的实现从而确定执行中的线程的操作是否合法.
- 程序遵循线程内语义: 在线程隔离状态下,每个线程的操作必须由该线程的语义控制,但是每个读操作看到的值由内存模型决定.
JMM规范
- 从数据存储上,对于共享数据的读写操作,JMM会通过线程工作内存以及JVM的堆内存来对数据进行读写操作(类比互联网业务,工作内存类比为redis等缓存,堆内存类比为数据储存载体,如数据库)
- 从代码优化上,JMM为了提升性能,会针对程序顺序代码进行重排序甚至删除不必要的同步代码
JMM概要
- 给定程序以及一个检测程序是否合法的执行跟踪,JMM工作原理是检查执行跟踪中的每个读,并根据某些规则检查读观察到的写是否有效
- 主要保证执行的每个结果与内存模型预期值一致,那么它可以不关心实现者是如何实现程序的行为
- 内存模型决定在程序的每个点可以读取哪些值。在隔离状态下,每个线程的操作必须由该线程的语义控制,但是每个读操作看到的值由内存模型决定
- 每次在线程内对变量进行写行为产生一个线程间的动作时,它必须匹配程序顺序中紧随其后的读行为的线程间动作,其中对于线程的读操作行为获取的值是由于JMM决定的值
2. JMM与顺序一致性模型
程序顺序与顺序一致性
- 程序顺序
- 可描述为线程间所有动作是根据线程内语义执行操作顺序的一个集合
- 简言之,就是在线程内的操作所见即所得,即程序代码顺序
- 顺序一致性内存模型
- 一个线程所有操作都必须按照程序的顺序来执行
- 不论线程是否同步,所有线程都只能看到一个单一的操作执行顺序,并且每个操作都必须是原子性操作并立即对其他所有线程可见
- 顺序一致性问题
- 如果内存模型使用一致性模型,则将会导致编译器和处理器的优化策略变得不合法
JMM在顺序一致性方面的努力
- 源代码
// shared.java int pwrite = 0; int cwrite = 0; // producer.java int pread = 0; int r1 = 0; run(){ r1 = 20; // --- 1 pread = cwrite; // --- 2 pwrite = 10; // --- 3 } // consumer.java int cread = 0; int r2 = 0; run(){ cread = pwrite; // --- 4 r2 = 21; // --- 5 cwrite = 20; // --- 6 }
- 代码分析
- 基于JMM模型: 由于存在数据竞争,上面的代码执行顺序会在编译器阶段,JMM允许对程序代码进行重排序,输出结果会出现pread = cwrite = 20 与 cread = pwrite = 10的情况
- 基于一致性内存模型: 将会正常输出,不会出现pread = cwrite = 20 与 cread = pwrite = 10的情况,但是线程之间的顺序会交替执行
- 加锁方案
- 基于JMM模型: 保证输出结果的正常,但是在上述线程内执行的顺序会被重排序
- 一致性内存模型: 不会打乱顺序,仍然正常结果输出
- 小结
- 在存在数据竞争的条件下,JMM无法保证线程之间的执行顺序,而顺序一致性保证与代码执行的顺序相同,即使线程的执行顺序存在交替执行也不影响单个线程内的执行顺序
- 单个线程中,JMM仍然会对临界区的执行动作进行重排序,而顺序一致性并没有进行重排,仍然保持与程序代码相同的顺序
3. JMM规范梳理
共享数据规则
- 能够被多个线程共享的内存区域称为共享内存或是堆内存
- 线程共享数据: 所有的对象实例字段,static字段,数组元素等
- 线程封闭数据: 局部变量,方法参数,异常处理器以及ThreadLocal/ThreadLocalRandom等
线程操作规则
相当于线程行为可以被其他线程看到,也可以检测到其他线程的行为动作,程序行为表现如下:
- 可以执行正常的读操作
- 可以执行正常的写操作
- 对于同步代码块
- 可以执行volatile数据的读取,说明其他线程的写操作当前线程可以“看到”(写操作在线程失效直接读取主内存)
- 可以执行volatile数据的写,说明变量数据可以对其他线程“可见”
- lock锁定监视器
- unlock解锁监视器
- 线程合并执行的第一个和最后一个动作(个人理解为等待多个线程执行子任务之后再一同执行程序后续代码的场景)
- 启动线程和终止线程
Synchronization原则(能够被感知,可见行为的变化)
- 监视器m的解锁与监视器m的后续动作加锁操作同步
- 线程对volatile变量v进行写操作,与任何线程对v的所有后续读操作同步
- 启动线程的操作与线程执行的第一个动作的操作同步
- 在线程中对每个属性执行默认值的写入操作与线程的第一个动作操作同步
- 线程中的最终动作T1 与另一个线程T2中检测到T1已终止的任何动作同步
- 如果线程T1中断thread T2,则该中断线程T1 将与 任何其他线程(包括T2)确定T2已被中断(通过InterruptedException引发或调用Thread.interrupted 或Thread.isInterrupted)的任何点同步
Happen-Before原则(规范)
- 执行动作之间的happen-before关系 如果两个动作x和y,我们定义hb(x,y)来描述x happen before y,满足关系有以下情况:
- 同一个线程中执行动作x和y,在程序顺序上x优先于y,则hb(x,y)
- 对象构造器代码块的结束happen-before该对象finalizer的开始
- 如果x动作与接下来的y动作同步,则hb(x,y)
- 传递性,如果hb(x,y),且hb(y,z),则hb(x,z)
- happen-before原则 主要作用于两个的动作存在冲突的执行顺序以及定义数据竞争发生的时机,具体VM实现,遵循以下原则:
- 线程解锁动作都happen-before该线程的后续动作锁操作
- 对volatile变量执行写操作happen-before该变量读操作的后续每个动作
- 调用线程的start()方法happen-before于已开启的线程内的任何一个动作
- 线程所有动作happen-before于其他任意线程成功从该线程对象的join()方法返回
- 程序任何对象的初始化happen-before于程序中任何其他的动作操作行为
- 作用
- 遵循上述的原则,意味着有些代码不能进行重排序,有些数据不能被缓存(解决JMM可见性的规范)
感谢花时间阅读,如果有用欢迎转发或者点个好看,谢谢!!!