JVM---Java内存模型
前言
计算机并发执行多个任务 与 充分利用计算机CPU性能 的关系 没有想象的简单,运算任务不可能只靠CPU就完成,CPU至少需要 与 内存进行交互(读写数据),IO操作无法消除;
但CPU的运算速度 与 存储设备的 速度 有几个数量级的差距;
所以又引入 接近CPU运算速度的高速缓存Cache,让运算快速进行,当运算结束后 从Cache同步到内存;
但 引入Cache后,又引入 缓存一致性问题;
在多CPU的系统中,多个CPU都有自己的Cache 又 共享同一个内存,将会导致各自的缓存数据不一致;如果发生缓存数据不一致,以哪个CPU的Cache为准?
为了解决缓存一致性问题,各CPU访问Cache时,要遵循一定的协议(MSI、MESI...),读写按照协议进行;
“内存模型” 可以理解为 在特定操作协议下,对特定内存或缓存 进行读写访问的 抽象过程;
不同架构的物理机器可以有不同的内存模型;
JVM也有自己的内存模型;
除了增加Cache外,为了使CPU的运算得到充分利用,CPU会对 输入的指令 进行 乱序执行优化 ,CPU会将乱序执行后的结果重新组装,保证结果和顺序执行一致;
Java内存模型(Java Memory Model,JMM)
概述
JVM规范视图 定义 一种JMM 屏蔽各种硬件和OS的 内存访问差异,以实现Java程序在任何平台下都能达到一致的内存访问效果;
C/C++ 直接使用物理硬件和OS的内存模型,由于平台上内存模型不同,所以在不同平台需要不同的程序实现;
jdk1.5后,JMM已经逐渐成熟和完善;
主内存&工作内存
JMM的主要目标:定义 程序中各个变量的 访问规则(在JVM中,将变量存储到内存 、从内存读取变量的底层细节);
(此处的变量,指实例字段、static字段...,不包括局部变量、方法参数 [线程私有])
为了获得更好的执行效能,JMM没有限制 执行引擎使用特定的寄存器或缓存 与 内存交互,也没限制JIT调整代码执行顺序进行优化;
JMM规定 所有的变量都存储在主内存中;
每个线程有自己的工作内存,
线程的工作内存 保存了被该线程使用的变量的主内存副本;
线程对 变量的读写操作 必须 在工作内存中进行(不能直接读写主内存的变量);
不同线程之间 不能直接访问 对方工作内存的变量;
不同线程之间 变量值的传递 只能通过 主内存来完成;
主内存&工作内存 交互协议
交互协议:一个变量 如何从主内存 拷贝到 工作内存、如何从工作内存 同步 到主内存;
JMM定义了8种操作来完成,每种操作都是原子操作:
Lock:
作用于 主内存变量;
把一个变量 标识为 一条线程独占状态;
unLock:
作用于 主内存变量;
把一个处于锁定状态的变量释放出来,释放后的变量才能被其他线程Lock;
read(读取):
作用于 工作内存变量;
把 主内存的变量值 传输到 线程的工作内存,以便后面的load使用;
load(载入):
作用于 工作内存变量;
把Read操作 从主内存获取的变量值 存储到 线程工作内存的变量中;
use(使用):
作用于 工作内存变量;
把 工作内存变量值 传递给 执行引擎;
assign(赋值):
作用于 工作内存变量;
把 从执行引擎 接收到的值 赋值给 线程工作内存变量;
store(存储):
作用于 工作内存变量;
把 工作内存变量的值 传送到 主内存中,以便后面的write使用;
write(写入):
作用于 主内存变量;
把 store操作 从 工作内存获得的变量值 写入到 主内存变量中;
把一个变量 从主内存 copy到 工作内存,需要顺序执行Read、load操作;
把一个变量 从工作内存 同步到 主内存,需要顺序执行store、write操作;
(注意:JMM只要求上述2对必须顺序执行,没有保证连续执行)
除此之外,JMM还规定在执行上述8个操作时必须满足以下规则:
...
这8种操作+上述规则+volatile特殊规定,已经完全确定Java程序哪些内存访问操作在并发下是安全的;
由于这种定义相当严谨但又繁琐,实践起来很麻烦,后面会介绍一个等效判断原则---先行发生原则,用来确定一个操作在并发下是否安全;
volatile变量的特殊规则:
volatile可以说是JVM提供的 最轻量级的 同步机制;
JMM对volatile变量定义了特殊访问规则:
当一个变量定义为volatile后,将具备2种特性:
1、保证此volatile变量 对所有线程的 可见性(当任一线程对该变量修改后,新值对其他线程是立即得知的);
(volatile变量在各个线程的工作内存不存在一致性问题 [volatile变量在各个线程的工作内存中可以存在不一致,但 每次使用前都要先刷新,执行引擎看不到不一致情况])
(但 Java程序的运算 并非原子操作,导致 volatile变量在并发下一样是不安全的)
eg:
static int oo =0; oo++; javap -v **.class 0: getstatic #2 3: iconst_1 4: iadd 5: putstatic #2
Java程序的volatile i , i++ ,用javap反编译后,由多条指令完成的,虽然get时保证了 i 值的正确性,但 iconst、innc指令由于并发执行,
其他线程已经对i值进行修改;
由于volatile只能保证多线程的可见性,在不符合以下规则的远算场景,还需要加锁(synchronized或原子类工具)保证原子性:
运算结果 不依赖 变量的当前值;
该变量没有包含在具有其他变量的不变式中;
2、禁止指令重排序优化
硬件架构上讲,指令重排序指CPU采用了允许将 多条指令 不按程序规定的顺序 分开远算处理;
(CPU需要能正确 处理指令依赖情况,保证 程序得出正确的计算结果 [指令间有依赖关系的无法重排序])
volatile修饰变量后,赋值指令后增加一条内存屏障指令(指令重排序时,不能把后面的指令重排序到内存屏障之前)
volatile变量性能
volatile变量 真的比 使用其他同步工具 更快么?
某些情况下,volatile的同步机制 性能优于 锁;
但JVM对锁实行的许多消除和优化,使得很难量化地认为volatile就比锁性能好;
如果volatile的读和写对比,读操作和普通变量几乎没什么差别,但是写操作会慢很多(需要在本地代码中插入许多内存屏障指令保证CPU不发生乱序执行);
JMM特征
JMM是围绕着 在并发过程中 如何处理 原子性、可见性、有序性 3个特征来建立的:
原子性
由JMM保证 原子性的变量操作 包括:Read、load、assign、use、store、write,大致认为对 基本数据类型 的读写具备原子性(long、Double除外);
如果应用场景需要更大范围的原子性保证,JMM还提供了Lock、unlock操作;
(JVM并没有将Lock、unlock开放给用户使用,但提供了更高层次的字节码指令monitorenter、monitorexit隐式使用,反映到Java程序中就是synchronized)
可见性
JMM通过 工作内存中修改变量值 后 同步到 主内存中,在读取变量值 从主内存中获取;
Java实现可见性:
volatile:
变量 特殊规则 保证新值能立即同步到主内存中,且 每次使用工作内存变量值 都从 主内存中重新获取;
synchronized:
在对一个变量执行unlock前,必须先把 工作内存的变量值 同步到 主内存中;
final:
被final修饰的变量 在构造器中一旦初始化完成, 且 构造器没有把this传递出去,其他线程就能看到final的值;
(this引用逃逸是件很可怕的事情,其他线程有可能使用这个引用访问到初始化一半的数据)
有序性
如果在本线程内观察,所有的操作都是有序的(线程内表现为串行);
如果在一个线程中观察另一个线程,所有的操作都是无序的(指令重排序现象 & 工作内存与主内存 同步延迟现象);
Java提供了 synchronized、volatile保证线程之间操作的有序性:
volatile:禁止指令重排序
synchronized:主内存的变量 在同一时刻 仅允许一条线程 对其进行Lock操作;
先行发生原则(happens-before)
如果JMM中所有的 有序性 都要靠volatile和synchronized来实现,那么操作将会变得很繁琐;
但是在编写Java并发代码时,并没有感觉到这点,因为Java语言有个先行发生(happens-before)原则;
这个原则非常重要,它是判断数据是否存在竞争、线程是否安全的主要依据;
先行发生原则:
JMM中定义的两项操作之间的偏序关系;
(如果操作A 先行发生于 操作B,其实就是说 在发生操作B之前,操作A产生的影响 [修改共享变量值、发送消息、调用方法...] 能被操作B观察到)
JMM下 先行发生 关系:
(这些先行发生关系 无需 任何同步器协助 就已经存在,可以在编码中直接使用;
如果2个操作之间的关系不在此列,且 无法从下列规则 推导出来,就没有顺序性保证,JVM可以对它们随意重排序)
程序次序规则:
同一个线程内,按照程序代码的顺序,书写在 前面的操作 先行于 后面的操作(严格说是 控制流顺序);
管程锁定规则:
对于同一个锁,一个线程的unlock操作 先行发生于 后面另一个线程对同一个锁的Lock操作;
volatile变量规则:
一个线程对volatile变量 的写操作 先行于 后面另一个线程对这个volatile变量的读操作;
线程启动规则:
Thread对象的start方法 先行于 此线程的每个操作;
线程终止规则:
线程中的所有操作都 先行于 对此线程的终止检测;
线程中断规则:
对线程interrupted方法的调用 先行于 被中断线程的代码检测到中断事件的发生;
对象终结规则:
一个对象的初始化完成 先行于 finalize方法的开始;
传递性:
如果操作A 先行于 操作B,操作B 先行于 操作C,可以得出 操作A 先行于 操作C;
时间先后顺序 与 先行发生原则 没有太大关系,衡量并发安全问题时 不要受到时间先后顺序的干扰,必须 以先行发生原则 为准;