《深入理解java虚拟机——JVM高级特性与最佳实践》阅读笔记 JAVA内存模型

引入高速缓存解决了处理器与内存间巨大的读写速度差异造成的效率低下问题,但由此也引入了缓存一致性的问题。
每个处理器都有自己的高速缓存,但是所有的处理器共用同一个内存,由此带来缓存数据不一致的问题。解决方法是规定处理器访问缓存时必须遵守的协议。
此外,为了使处理器内部的运算单元被充分运用,处理器会对输入代码进行乱序执行的优化操作。
处理器在计算执行完毕后会对乱序结果进行重组,保证最后结果与预期一致,但是计算顺序不一定一致。由此带来的问题是,如果一个计算任务的结果依赖于另一个计算任务的中间结果,光靠代码顺序无法保证正确执行。因此,JVM的即时编译器中也引入了指令重排序的优化机制。
也就是说,处理器打乱一次顺序,即时编译器再将顺序重新排好

JAVA虚拟机规范定义了内存模型以屏蔽硬件间的内存访问差异,主要目标是定义程序中变量的访问规则,也就是虚拟机与内存间的变量存取操作(此处为广泛意义上的变量,包括静态字段,实例字段以及组成数组对象的元素)。
内存模型规定变量存储在主内存中(主内存为JVM内存的一部分,不是物理意义上的内存),线程使用到的变量副本则存储在线程私有的工作内存中,线程间的变量传递要经由主内存完成。

主内存与工作内存间的操作(要求都具备原子性)分为两种:

主内存:

操作名作用
lock将变量标识为线程独占状态
unlock将变量从线程独占状态释放
read将变量传入工作内存
write将store操作从工作内存中得到的值存入主内存变量中

虚拟机并未将 lock 与 unlock 操作开放给用户使用,而是选择用字节码指令 monitorenter 以及 monitorexit 来隐式调用。在 java 代码中即为 synchronized 关键字
工作内存:

操作名作用
load将read操作从主内存得到的变量值放入工作内存变量副本中
use将工作内存中的变量值传给执行引擎(虚拟机遇到需要使用变量值的字节码指令时执行此操作)
assign将从执行引擎接收到的值赋给工作内存变量(虚拟机遇到给变量赋值的字节码指令执行此操作)
store将工作内存中变量的值传递给主内存变量

执行基本操作时需要遵循的规则:

序号规则内容
1read 和 load,store 和 wrte,必须成对出现。即不允许有 read 无 load,有 store 无 write 的情况
2不允许线程丢弃最近的 assign 操作,即工作内存中出现变量变化必须同步回主内存
3不允许线程在没有任何 assign 操作情况下将变量从工作内存同步回主内存
4新变量只能从主内存产生,不允许在工作内存直接使用未初始化变量。即对变量实行 use、store 操作之前,必须经过 assign、load 操作
5一个变量在同一时刻只允许一条线程对其进行 lock 操作,并且 lock 操作可被同一重复线程执行多次。但重复执行多次后,必须执行相同次数的 unlock 操作才能解锁
6执行 lock 操作,将会导致工作内存清空此变量值,在执行引擎使用变量前,需要重新将变量值初始化
7如果变量事先没有被 lock 锁定,就不允许对其执行 unlock 操作,也不允许对其它线程 lock 的变量进行该操作
8在对变量执行 lock 之前,必须先将其同步回主内存中

线程中对 volatile 变量执行操作需遵循的规则:

序号规则
1load 与 use 操作必须连续出现。即 load 操作执行后必须执行 use,要执行 use,必须先执行 load。并且为了保证变量能及时反映出最新的变化,在执行操作前必须先从主内存获取最新的变量值
2store 与 assign 必须连续出现,具体规则与上一条相同
3假设动作1和动作2是对变量1的一组关联动作(如 use 和 load ),动作3是对变量1的 read 或 write 动作;动作A和动作B是对变量2的一组关联动作,动作C是对变量2的 read 或 write 动作。如果动作1先于动作A,那么动作3先于动作C。(此规则要求 volatile 变量不会被指令重排优化,保证代码执行顺序与编写顺序相同。)

针对 long 和 double 类型变量的特殊规则:

规则名称内容
非原子性协定虚拟机可以将未被 volatile 修饰的64位数据的读写操作划分为两次32位操作来进行(也就是可以不保证 load、store、write 以及 read 这四种操作的原子性)

目前商用虚拟机都选择将64位数据的读写操作实现为原子操作,因此使用 double 与 long 时无需特别声明为 volatile。

主内存复制到工作内存,要顺序执行 read 和 load 操作;从工作内存同步回主内存,则需要顺序执行store 和 write 操作。以上操作需要按顺序执行,但不需要保证连续。也就是在两个命令间能够插入其它指令。

普通变量值的修改需要存入主内存并被其它线程获取后才能让其被得知,但是使用 volatile 修饰的变量,可以让变量不经过此步骤也能被其它线程得知。并且 volatile 变量不存在一致性问题。但由于 Java 内运算并非原子操作,因此 volatile 变量在并发环境下并不安全
volatile禁止指令重排优化。也就是在虚拟机内部代码执行顺序保证和编写顺序一致。
指令重排序,指CPU将多条指令不按程序规定的顺序分开发送给各相应电路单元处理(仍然需要正确处理指令依赖情况,也就是有依赖关系的指令间执行顺序不变。)。
volatile 读操作和普通变量差不多,但写操作显得有些慢(需要在本地代码插入内存屏障指令保证不发生乱序执行),但大多数场景下,开销依然比锁低。

本线程内观察,所有操作有序;在另一个线程中观察,所有操作无序。即为 “线程内表现为串行的语义”、“工作内存与主内存同步延迟现象” 以及 “指令重排序”。

volatile、final、synchronized 三者可见性对比:

关键字可见性来源
volatile让变量的更改操作无需经过主内存也能被其它线程得知
synchronized对变量执行 unlock 操作前,必须先将变量同步回主内存中
final被修饰的字段在构造器内初始化完成,且构造器未将 “this” 的引用传递出去,其它线程就能够看见被修饰的值

volatile 与 synchronized 有序性对比:

关键字有序性来源
volatile禁止指令重排的语义
synchronized一个变量在一个时刻只允许一条线程对其进行 lock 操作

先行发生原则(无需任何同步手段,JVM默认执行)

规则名称具体含义
传递性操作 A 先发生于操作 B ,操作 B 先发生于操作 C ,则可得出操作 A 先发生于操作 C 的结论
管程锁定规则unlock 操作先行发生于对之后同一个锁的 lock 操作
线程启动规则Thread 对象的 start() 方法先行发生于此线程的每一个动作
线程中断规则对线程 interrrupt() 方法的调用先行发生于被中断线程的代码检测到中断事件的发生(通过 Thread.interrupted() 方法检测是否有中断发生)
线程终止规则线程中所有操作都先于终止检测执行(通过 Thread.join() 方法结束、Thread.isAlive() 的返回值进行检测
对象终结规则对象初始化完成先行发生于 finalize() 开始
程序次序规则在一个线程内,按照控制流顺序,书写在前面的操作要先于后面的操作执行
volatile 变量规则对同一个 volatile 变量的写操作优先于后面对同一个变量的读操作

时间先后顺序与先行发生原则间基本没有关系,因此考虑并发安全问题时以先行发生原则为准
注意:如果没有一条先行原则符合,则传递性无从谈起

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值