java线程之内存模型
参考书籍: <1. 并发编程模型的两个关键问题 在并发模型中, 常常要处理两个关键的问题:>
这篇文章是自己阅读该书籍时的读书笔记
- 线程之间如何通信
- 线程之间如何同步
通信 : 线程之间以何种机制来交换信息;
同步: 线程中用于控制不同线程之间操作发生相对顺序的机制
- 共享内存
在共享内存的并发模型里, 线程之间共享程序的公共状态, 通过写-读内存中的公共状态来进行隐式通信; - 消息传递
在消息传递的并发模型里, 线程之间没有公共状态, 线程之间必须通过发送信息来显示进行通信;
在共享内存并发模型中, 同步是显示进行的;2. java内存模型的抽象结构
在消息传递并发模型中, 同步是隐式进行的;
java的并发采用的是共享内存模型(因此需要显示进行同步)
文章图片
java内存模型的抽象结构示意图 3. 数据依赖性 如果两个操作访问同一个变量, 且这两个操作中有一个为写操作, 此时这两个操作之间就存在数据依赖性
文章图片
数据依赖类型
注意:4. 重排序 重排序 是指编译器和处理器为了优化程序性能而对指令序列进行重新排序的一种手段;
编译器和处理器在重排序时, 会遵守数据依赖性, 编译器和处理器不会改变存在数据依赖关系的两个操作的执行顺序;
这里所说的数据依赖性仅针对单个处理器中执行的指令序列和单个线程中执行的操作, 不同处理器之间和不同线程之间的数据依赖性不被编译器和处理器考虑;
文章图片
重排序的类型
注意: 内存系统的重排序可能导致处理器对内存的写/读操作的执行顺序不一定与内存实际发生的读/写操作顺序一致
对于编译器的重排序, JMM的编译器重排序规则会禁止特定类型的编译器重排序;5. 顺序一致性内存模型(理论模型 参考模型) 顺序一致性内存模型的两大特性:
对于处理器的重排序, JMM的处理器重排序规则会要求java编译器在生成指令序列时, 插入特定类型的内存屏障(Memory Barriers)指令, 通过内存屏障指令来禁止特定类型的处理器重排序
文章图片
内存屏障的类型
JMM这样做的目的是: 为程序员提供一致的内存可见性的保证
- 一个线程中的所有操作必须按照程序的顺序来执行;
- (不管程序是否同步)所有线程都只能看到一个单一的操作执行顺序, 在顺序一致性内存模型中, 每个操作都必须原子执行且立刻对所有线程可见;
文章图片
顺序一致性内存模型为程序员提供的视图
注意: 正确同步的多线程程序的执行结果与该程序在顺序一致性内存模型中的执行结果相同(即可以利用程序在顺序一致性内存模型的执行结果来判断自己编写的多线程程序是否符合期望)6.
volatile
的内存语义
volatile
的特性
可见性: 对一个volatile
变量的读, 总是能看到(任意线程)对这个volatile
变量最后的写入;
原子性: 对任意单个volatile
变量的读/写具有原子性, 但类似于volatile++这种复合操作不具有原子性;
volatile
写-读的内存语义
当写一个volatile
变量时, JMM会把该线程对应的本地内存中所有的共享变量的值刷新到主内存;
当读一个volatile
变量时, JMM会把该线程对应的本地内存置为无效, 线程接下来从主内存中读取共享变量;
-
volatile
内存语义的实现
JMM针对编译器指定的volatile重排序规则
文章图片
编译器的volatile重排序规则
- 当第二个操作是
volatile
写 时, 不管第一个操作是什么, 都不能重排序(这个规则确保volatile
写 之前的操作不会被编译器重排序到volatile写
之后) - 当第一个操作是
volatile
读 时, 不管第二个操作是什么, 都不能重排序(这个规则确保volatile
读 之后的操作不会被编译器重排序到volatile
读 之前; - 当第一个操作是
volatile
写, 第二个操作是volatile
读 时, 不能重排序;
为了实现volatile
的内存语义, JMM采取保守策略, 编译器在生成字节码时, 会在指令序列中插入内存屏障来禁止特定类型的处理器重排序;
文章图片
处理器的volatile重排序规则
文章图片
volatile写的指令序列示意图
文章图片
volatile读的指令序列示意图
- 当第二个操作是
- 锁的释放和获取的内存语义
当线程释放锁时, JMM会把该线程对应的本地内存中的共享变量刷新到主内存中;
当线程获取锁时, JMM会把线程对应的本地内存置为无效;
注意: 锁释放与volatile写有相同的内存语义; 锁获取与volatile读有相同的内存语义;
- 锁释放-获取的内存语义的实现
- 利用
volatile
变量的写-读所具有的内存语义; - 利用 CAS 所附带的
volatile
读 和volatile
写的内存语义;
- 利用
final
域的内存语义
-
final
域的重排序规则
- 在构造函数内对一个
final
域 的写入, 与随后把这个被构造对象的引用赋值给一个引用变量, 这两个操作之间不能重排序; (写
final
域) - 初次读一个包含
final
域 的对象的引用, 与随后初次读这个final域, 这两个操作之间不能重排序; (读final
域)
- 在构造函数内对一个
- 写
final
域的重排序规则
写final
域的重排序规则禁止把final
域的写重排序到构造函数之外, 实现该规则需要:
- JMM禁止编译器把
final
域的写重排序到构造函数之外; - 编译器会在
final
域的写之后, 构造函数return之前, 插入一个StoreStore
屏障(这个屏障禁止处理器把final
域的写重排序到构造函数之外)
- JMM禁止编译器把
写final
域的重排序规则可以保证在对象引用为任意线程可见之前, 对象的final
域已经被正确初始化过了
- 读
final
域的重排序规则
读final
域的重排序规则禁止处理器重排序初次读一个包含final
域对象的引用和初次读这个final
域, 实现该规则需要:
编译器在读final
域操作的前面插入一个LoadLoad
屏障
读final
域的重排序规则可以保证在读一个对象的final
域之前, 一定会先读包含这个final
域的对象的引用
- 写引用类型的
final
域的额外重排序规则
对于引用类型, 写final
域的重排序规则对编译器和处理器增加了如下约束:
在构造函数内对一个final
引用的对象的成员域的写入, 与随后在构造函数外把这个被构造对象的引用赋值给一个引用变量, 这两个操作之间不能重排序
注意:9. happens-before 规则(JMM对程序员的承诺) 1). 程序顺序规则final
的内存语义向程序员保证了只要对象是正确构造的(被构造对象的引用在构造函数中没有"逸出")则不需要使用同步就可以保证任意线程都能看到这个final
域在构造函数中被初始化之后的值
一个线程中的每一个操作, happens-before 于该线程中的任意后序操作;
2). 监视器锁规则
对一个锁的解锁, happens-before 于随后对这个锁的加锁;
3). volatile变量规则
对一个volatile域的写, happens-before于任意后续对这个volatile域的读;
4). 传递性
如果A happens-before B, 且B happens-before C, 那么 A happens-before C;
5). start()规则
如果线程A执行操作ThreadB.start()(启动线程B), 那么A线程的ThreadB.start()操作 happens-before 于线程B中的任意操作;
6). join()规则
如果线程A执行操作ThreadB.join()并成功返回, 那么线程B中的任意操作 happens-before 于线程A从 ThreadB.join()操作成功返回(的后序操作);
7). 线程终结规则
线程中所有的操作都先行发生于线程的终止检测;
【java线程之内存模型】8). 对象终结规则
一个对象的初始化完成先行于它的finalize()方法的开始;
推荐阅读
- JAVA(抽象类与接口的区别&重载与重写&内存泄漏)
- PMSJ寻平面设计师之现代(Hyundai)
- 太平之莲
- 闲杂“细雨”
- 七年之痒之后
- 深入理解Go之generate
- 由浅入深理解AOP
- 期刊|期刊 | 国内核心期刊之(北大核心)
- 生活随笔|好天气下的意外之喜
- 感恩之旅第75天