导图社区 Java内存模型
这是一篇关于Java内存模型的思维导图,其内容涵盖了内存模型,happers before,重排序,内存语义以及as if serial
编辑于2022-06-05 17:06:18Java内存模型
内存模型
Java内存模型基础
并发编程模型(线程之间两个关键问题)
通信
指:线程之间以何种机制来交换信息
通讯机制(命令式编程)
命令式编程:用详细的命令机器怎么去处理一件事情以达到你想要的结果.声明式编程:简单的说,你只需要告诉计算机,你要得到什么样的结果,计算机则会完成你想要的结果
共享内存
消息传递
并发模型
共享内存并发模型
线程之间共享程序的公共状态
通过写-读内存中的公共状态进行 隐式通信
消息传递并发模型
线程之间没有公共状态
线程之间通过发送消息 显式通信
同步
指:控制不同线程间操作发生相对顺序的机制
并发模型
共享内存并发模型:同步是显式进行
必须显式️指定某个方法或某段代码需要在线程之间互斥执行
消息传递并发模型:同步是隐式进行
消息的发送必须在消息的接收之前
Java
Java通信机制
并发采用:共享内存模型
通信:总是隐式进行
Java内存模型(简称:JMM)的抽象结构
JMM 控制线程之间的通信
JMM 决定一个线程对共享变量的写入何时对另一个线程可见
通过:控制主内存与每个线程的本地线程之间的交互,为Java保证内存的可见性
JMM定义了线程和主内存之间的抽象关系
线程之间的共享变量存储在主内存(Main Memory)中,每个线程都有一个私有的本地内存(Local Memory),本地内存中存储该线程以读/写共享变量的副本。
本地内存 是JMM抽象概念
缓存
写缓存
寄存器
其他硬件、编译器优化
涵盖
源码到指令序列的重排序
1. 编译器优化
编译器重排序
2. 指令并行
现代处理器指令并行技术(Instruction-Level Parallelism,ILP)
3. 内存系统
处理器重排序
内存屏障
LoadLoad
StoreStore
LoadStore
StoreLoad
“全能型”
内存模型分类
数据竞争(非正确同步)
顺序一致性:程序的执行结果与该程序在顺序一致性内存模型中执行的结果相同。
内存模型
顺序一致性内存模型:理论参考模型
为程序员提供极强的内存可见性保证
特性
一个线程中的所有操作必须按照程序的顺序来执行
(不管程序是否同步)所有线程都只能看到一个单一的操作执行顺序。
在顺序一致性模型中,每个操作都必须原子执行且立刻对所有线程可见。
同步程序的顺序一致性效果
顺序一致性模型中:所有操作完全按程序的顺序串行执行。
JMM在具体实现上基本方针:在不改变程序的执行结果前提下,尽可能地为编译器和处理器的优化打开方便之门。
未同步(未正确同步)程序的执行特性
JMM 只提供最小安全性
线程执行时读取到值,要么是之前某个线程写入的值,要么是默认值(0,null ,false) JMM 保证线程读操作读取到的值不会无中生有(out of thin air)的冒出来。 为了实现最小安全性,JVM在堆上分配对象时,首先会对内存空间进行清零,然后才会在上面分配对象(JVM内部会同步这两个操作)。因此,在已清零的内存空间分配对象式,域的默认初始化已经完成。
JMM不保证与该程序在顺序一致性模型中的执行结果一致(无意义)
JMM中执行时,整体无序,执行结果无法预知。
JMM与顺序一致性模型差异
顺序一致性模型:保证单线程内的操作会按程序的顺序执行JMM:不保证单线程内的操作按程序的顺序执行
顺序一致模型:保证所有线程只能看到一致的操作执行顺序JMM:不保证所有线程看到一致的执行顺序
JMM:不保证64位的long型与duble型变量的写操作具有原子性顺序一致性模型:保证对所有内存读/写操作都具有原子性
JDK5开始,仅仅允许把一个64位long/duble类型变量的写操作拆分为两个32位的写操作执行,任意的读操作必须具有原子性。
差异与处理器总线的工作机制密切相关。数据通过总线在处理器和内存之间传递。
处理器内存模型:硬件级
类型
放松程序中写-读操作的顺序(Total Store Ordering 内存模型,TSO)
在TSO基础上,继续放松程序中写-写操作的顺序(Partial Store Order内存模型,PSO)
在TSO、PSO基础上,继续放松程序中读-写和读-读操作的顺序(Relaxed Memory Order 内存模型「简称:RMO」和PowerPc内存模型)
读-写操作放松,是两个操作之间不存在数据依赖性为前提
处理器内存模型、编程语言内存模型 的参照
处理器内存模型都允许 写-读 重排序
写缓冲区导致
JMM内存模型:语言级 (编程语言内存模型)
对正确同步,程序的执行具有顺序一致性
常用同步
synchronized
volatile
final
JUC
未同步程序在JMM中,整体的执行顺序无序,所有线程看到的执行顺序也可能不一致。
参考:顺序一致性模型
可见性保证
单线程程序
单线程程序不会出现内存可见性问题
正确同步的多线程程序
正确同步的多线程的执行将具有顺序一致性JMM关注的重点,JMM通过限制编译器、处理器的重排序为程序员提供内存可见性保证
未同步/未正确同步的多线程程序
最小安全性保障
happens-before
JMM核心概念
JMM设计意图
程序员对内存模型的使用
强内存模型编写代码
编译器、处理器对内存模型的实现
弱类型模型
互相矛盾,JMM核心目标找到平衡点
阐述:操作之间的内存可见性,JMM中,如一个操作执行的结果对另一个操作可见,则两个操作之间必须存在happen-before关系(两个操作可以是单线程或不同线程之间)。
JMM把happens-before要求禁止重排序分类
会改变结果
不会改变结果
定义
1. 如果一个操作happen-before另一个操作,那么第一个操作的执行结果将对第二个操作可见,而且第一个操作的执行顺序排在第二个操作之前。
JMM对程序员的承诺
2. 两个操作之间存在happen-before关系,并不意味这Java平台的具体实现必须要按照happen-before关系指定的顺序执行。如果重排序之后的执行结果,与按照happen-before关系来执行的结果一致,那么这种重排序不非法(JMM允许这种重排序)
JMM对编译器和处理器重排序的约束原则
规则
注意:并不意味前一个操作必须要在后一个操作之前执行,仅要求前一个操作(结果)对后一个操作可见,且前一个操作按顺序排在第二个操作之前。
程序顺序规则
监视器锁规则(解锁happens-before加锁)
volatile变量(写 happen-before 读)
传递性
start()规则
join()的规则
重排序
数据依赖性
如果两个操作访问同一个变量,且这两个操作中有一个为写操作,此时这两个操作之间就存在数据依赖性
写后读
写后写
读后写
重排序,会改变结果
编译器和处理器在重排序时,遵守数据依赖性
针对单个处理器中执行的指令序列、单线程执行的操作。
不同处理器之间、不同线程之间的数据依赖性,不被编译器和处理器考虑
as-if-serial语义
定义:不管怎么重排序,(单线程)程序的执行结果不能改变
编译器
runtime
处理器
必须遵守
重排序规则,不改变程序结果即可
多线程重排序可能会改变结果
重排序缓冲(Reorder buffer ,ROB),硬件缓存
内存语义
volatile
修饰引用类型,保证引用地址的可见性
使用AtomicReference封装替代volatile修饰引用类型变量。
特性
可见性
对一个volatile变量的读,总是能看到(任意线程)对这个volatile变量最后的写入。
原子性
对任意单个volatile变量的读/写具有原子性,对volatile++复合操作不具备原子性
volatile 与 happpens-before
volatile写 ==锁释放
volatile读==锁获取
相同的内存语义
写-读内存语义
volatile写内存语义:当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值刷新到主内存。
volatile读内存语义:当读一个volatile变量时,JMM会把该线程对象的本地内存置为无效。线程将从主内存中读取共享变量。
总结:
线程A写一个volatile变量,实质上是线程A向接下来将要读这个volatile变量的某个线程发出了(其对共享变量所做修改的)消息。
线程B读一个volatile变量,实质上线程B接收了之前某个线程发出的(在写这个volatile变量之前对共享变量所做的修改的)消息。
线程A写一个volatile变量,随后线程B读这个volatile变量,这个过程实质上是线程A通过主内存向线程B发送消息。
内存语义的实现
限制编译器、处理器重排序增加内存屏障
JDK5对volatile内存语义增强:严格限制编译器、处理器对volatile变量与普通变量的重排序
确保volatile写-读和锁的释放-获取具有相同的内存语义
锁(非Synchronized)
锁与 happpens-before
锁释放、获取内存语义
锁释放:JMM把该线程对应的本地内存中的共享变量刷新到主内存中。
锁获取:JMM把该线程对应的本地内存置为无效。 使得被监视器保护的临界区代码必须从主内存中读取共享变量。
总结:
线程A释放一个锁,实质上是线程A向接下来将要获取这个锁的某个线程发送了(线程A对共享变量所做修改的)消息。
线程B获取一个锁,实质上是线程B接收了之前某个线程发出的(在释放这个锁之前对共享变量所做修改的)消息。
线程A释放锁,随后线程B获取这个锁,这个过程实质上线程A通过主内存向线程B发送消息
内存语义的实现
AQS
volatile变量
CAS
实现关键
concurrent的实现
A线程写volatile变量,随后B线程读这个volatile变量。
A线程写volatile变量,随后B线程用CAS更新这个volatile变量。
A线程用CAS更新一个volatile变量,随后B线程用CAS更新这个volatile变量。
A线程用CAS更新一个volatile变量,随后B线程读这个volatile变量。
Java线程之间的通信
基于CAS同时具备volatile读、写内存语义
JUC通用化实现模式
1. 声明共享变量volatile
2. 使用CAS的原子条件更新来实现线程之间的同步。
3. 配合以volatile读/写和CAS锁具有的volatile读和写的内存语义来实现线程之间的通信。
final
final域,编译器和处理器遵守
在构造函数内对一个final域的写入,与随后把这个被构造函数对象的引用的复制给一个引用变量,这两个操作之间不能重排序。
初次读一个包含final域的对象的引用,与随后初次读这个final域,这个两个操作之间不能重排序。
写final域重排序规则
保证:对象引用为任意线程可见之前,对象的final域已经被正确初始化过了,而普通变量不具有这个保障(普通变量可以被重排序到构造函数之外)
JMM禁止编译器把final域的写重排序到构构造函数之外。
编译器会在final域的写之后,构造函数return之前,插入一个StoreStore屏障(禁止处理器把final域的写重排序到构造函数之外。)。
final域为引用类型增加约束: 在构造函数内对final引用的对象的成员域的写入,与随后在构造函数外把这个被构造的对象的引用赋值给一个引用变量,这两个操作不能重排序。
读final域重排序规则
针对处理器规则:在一个线程中,处理读这个对象引用与初次读该对象包含的final域,JMM禁止处理器重排序这个两个操作。
存在间接依赖关系,大多数处理器会禁止重排序间依赖关系
针对编译器规则:在读final域操作的前面插入一个LoadLoad屏障
final语义 在处理器中的实现
为什么增加final语义(JSR-133)
保证正确的初始化
as-if-serial /happens-before
两者的目的,保证不改变程序执行结果的前提,尽可能地提高程序执行的并行度
as-if-serial 语义保证单线程内程序的执行结果不被改变happens-before关系保证正确同步的对象成程序的执行结果不被改变。
as-if-serial语义 给编写单线程程序的程序员创造了一个幻读:单线程程序是按程序的顺序执行的。happens-before关系给编写正确同步的短线程程序的程序员创造了一个幻境:正确同步的多线程程序是按happens-before指定的顺序来执行。