引入高速缓存解决了处理器与内存间巨大的读写速度差异造成的效率低下问题,但由此也引入了缓存一致性的问题。
每个处理器都有自己的高速缓存,但是所有的处理器共用同一个内存,由此带来缓存数据不一致的问题。解决方法是规定处理器访问缓存时必须遵守的协议。
此外,为了使处理器内部的运算单元被充分运用,处理器会对输入代码进行乱序执行的优化操作。
处理器在计算执行完毕后会对乱序结果进行重组,保证最后结果与预期一致,但是计算顺序不一定一致。由此带来的问题是,如果一个计算任务的结果依赖于另一个计算任务的中间结果,光靠代码顺序无法保证正确执行。因此,JVM的即时编译器中也引入了指令重排序的优化机制。
也就是说,处理器打乱一次顺序,即时编译器再将顺序重新排好。
JAVA虚拟机规范定义了内存模型以屏蔽硬件间的内存访问差异,主要目标是定义程序中变量的访问规则,也就是虚拟机与内存间的变量存取操作(此处为广泛意义上的变量,包括静态字段,实例字段以及组成数组对象的元素)。
内存模型规定变量存储在主内存中(主内存为JVM内存的一部分,不是物理意义上的内存),线程使用到的变量副本则存储在线程私有的工作内存中,线程间的变量传递要经由主内存完成。
主内存与工作内存间的操作(要求都具备原子性)分为两种:
主内存:
操作名 | 作用 |
---|---|
lock | 将变量标识为线程独占状态 |
unlock | 将变量从线程独占状态释放 |
read | 将变量传入工作内存 |
write | 将store操作从工作内存中得到的值存入主内存变量中 |
虚拟机并未将 lock 与 unlock 操作开放给用户使用,而是选择用字节码指令 monitorenter 以及 monitorexit 来隐式调用。在 java 代码中即为 synchronized 关键字。
工作内存:
操作名 | 作用 |
---|---|
load | 将read操作从主内存得到的变量值放入工作内存变量副本中 |
use | 将工作内存中的变量值传给执行引擎(虚拟机遇到需要使用变量值的字节码指令时执行此操作) |
assign | 将从执行引擎接收到的值赋给工作内存变量(虚拟机遇到给变量赋值的字节码指令执行此操作) |
store | 将工作内存中变量的值传递给主内存变量 |
执行基本操作时需要遵循的规则:
序号 | 规则内容 |
---|---|
1 | read 和 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 变量执行操作需遵循的规则:
序号 | 规则 |
---|---|
1 | load 与 use 操作必须连续出现。即 load 操作执行后必须执行 use,要执行 use,必须先执行 load。并且为了保证变量能及时反映出最新的变化,在执行操作前必须先从主内存获取最新的变量值 |
2 | store 与 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 变量的写操作优先于后面对同一个变量的读操作 |
时间先后顺序与先行发生原则间基本没有关系,因此考虑并发安全问题时以先行发生原则为准
注意:如果没有一条先行原则符合,则传递性无从谈起