@TOC
Java内存模型(Java Memory Model,JMM) 是《Java虚拟机规范》中定义的一种用来屏蔽各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致性的内存访问效果的一种内存访问模型。从JDK5开始 JMM才正真成熟,完善起来。
Java内存模型的主要目的是定义程序中各种变量
(Java中的实例字段,静态字段和构成数组中的元素,不包括线程私有的局部变量和方法参数)的访问规则
。
Java内存模型规定了所有的变量都存储在主内存(Main Memory)
。每条线程都有自己的工作内存(Working Memory)
,用来保存被该线程使用的变量的主内存副本。线程对变量的所有操作(读取、赋值等)都必须在工作内存中进行,而不能直接读写主内存中的数据。不同线程之间无法之间访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成。
线程、主内存、工作内存三者的交互关系如下图。
关于一个变量如何从工作内存拷贝到工作内存,如何从工作内存同步回主内存这一类的实现细节,Java内存模型中定义了以下8种操作来完成。Java虚拟机实现时必须保证下面提及的每一种操作都是原子的、不可在分
的(对 double 和 long 类型的变量来说,load、store、read 和 write操作在某些平台上允许有例外)。
它们的使用规则如下:
Java内存模型中对volatile变量定义的特殊规则定义。假定T表示一个线程,V和W分别表示两个volatile型变量,那么在进行read、load、use、assign、store 和 write 操作时需要满足如下规则:
只有当线程T对变量V执行的前一个动作是 load 的时候,线程T才能对变量V执行use动作;并且,只有当线程 T 对变量 V 执行的后一个动作是 use 的时候,线程T才能对变量V执行load 动作。线程 T 对变量 V 的 load、read 动作相关联,必须连续且一起出现。
这条规则要求在工作内存中,每次使用 V 前必须先从主内存刷新最新的值,用于保证能看见其它线程对变量V所作的修改。
只有当线程 T 对变量 V 执行的前一个动作是 assign 的时候,线程 T 才能对变量V执行 store 动作;并且,只有当线程 T 对变量 V执行的后一个动作是 store 时候,线程T 才能对变量 V 执行 assign 动作。线程 T 对变量 V 的 assign 动作 可以认为是线程 T 对变量 V 的 store、write动作相关联的,必须连续且一起出现。
这条规则要求在工作内存中,每次修改 V 后 都必须立刻同步回主内存中,用于保证其它线程可以看到子集对变量 V 所做的修改。
假定动作A 是线程 T 对变量V实施的 use 或 assign 动作,假定动作 F 是和动作A相关联的 load 或 store 动作,假定动作 P 和动作 F 相应的对变 V 的 read 或 write 动作;与此类似,假定动作 B 是线程 T 对变量 W 实施的 use 或 assign 动作,假定动作 G 是和动作B 相关联的 load 或 store 动作,假定动作 Q是和动作G相应的对变量W的 read 或 write 动作。如果 A 先于B,那么 P先于Q。
这条规则要求 volatile 修饰的变量不会被指令重排序优化,从而保证代码的执行顺序与程序的顺序相同。
下面是Java内存模型下一些 "天然的" 先行发生关系。
在Java类库中提供的其它 Happens-Before 排序包括:
内存屏障(也称内存栅栏,屏障指令等,是一类同步屏障指令,是CPU或编译器在对内存随机访问的一个同步点,使得此点之前的所有读写操作操作都执行后才可以开始执行此点之后的操作),避免代码重排序。代码屏障其实就是一种JVM指令,Java内存模型的重排规则会要求Java编译器在生成JVM指令是插入特点的内存屏障指令,volatile实现了Java内存模型中的可见性和有序性(禁重排),但volatile 不能保证原子性。
内存屏障之前的所有写操作都要写回主内存
内存屏障之后的所有读操作都能获得所有内存屏障之前写操作的最新结果(实现了可见性)
写屏障(Store Memory Barrier):告诉处理器在写屏障之前将所有存储在缓存(store bufferes)中的数据同步到主内存。也就是说当看到Store屏障指令,就必须把该指令之前所有写入指令执行完毕才能继续往下执行。
读屏障(Load Memory Barrier):处理器在读屏障之后的读操作,都在读屏障之后执行。也就是说在Load屏障指令之后就能够保证后面的读取数据指令一定能够读取到最新的数据。
因此重排序时,不允许把内存屏障之后的指令重排序到内存屏障之前。一句话:对一个volatile变量的写,先行发生于任意后续对这个volatile变量的读,也叫写后读。
分类
内存屏障粗分有两种,分别是 读屏障(Load Barrier) 和 写屏障(Store Barrier)
读屏障(Load Barrier):在读指令之前插入读屏障,让工作内存或CPU高速缓存当中的缓存数据失效,重新回到主内存中获取最新数据。
写屏障 (Store Barrier):在写指令之后插入写屏障,强制把写缓冲区的数据刷回到主内存中。
再来看看源码 :首先打开 Unsafe.class ,发现都是本地方法
再打开其实现类:Unsafe.cpp
orderAccess.hpp
从上面的的源码可以看到,两种内存屏障又可以细分为四种,其作用如下:
屏障类型 | 指令说明 | 说明 |
---|---|---|
LoadLoad | Load1; LoadLoad; Load2 | 保证 Load1 的读取操作在 Load2 及其之后的读取操作之前执行 |
StoreStore | Store1; StoreStore; Store2 | 在 Store2 及其后的写入操作执行之前,保证 Store1 的写入操作已刷新到主内存 |
LoadStore | Load1;LoadStore; Store2 | 在 Store2 及其后的写入操作执行之前,保证 Load1 的读取操作已读取结束 |
StoreLoad | Store1; StoreLoad; Load2 | 在 Load2 及其后的读取操作执行之前 ,保证Store1 的写入操作已刷新回主内存 |
最后 orderAccess_linux_x86.inline.hpp
inline void OrderAccess: : loadload() { acquire(); }
inline void OrderAccess: : storestore() { release(); }
inline void OrderAccess: : loadstore() { acquire(); }
inline void OrderAccess: : storeload() { fence(); }inline void OrderAccess::acquire() {volatile intptr_t local_dummy;
#ifdef AMD64__asm__ volatile ("movq 0(%%rsp), %0" : "=r" (local_dummy) :: "memory");
#else__asm__ volatile ("movl 0(%%esp), %0" : "=r" (local_dummy) :: "memory");
#endif // AMD64
}inline void OrderAccess::release() {// Avoid hitting the same cache-line from// different threads.volatile jint local_dummy = 0;
}inline void OrderAccess::fence() {if (os::is_MP()){// always use locked addl since mfence is sometimes expensive
#ifdef AMD64__asm__ volatile ("lock; addl $0,0(%%rsp)" : : : "cc", "memory");
#else__asm__ volatile ("lock; addl $0,0(%%esp)" : : : "cc", "memory");
#endif}
}
关键字 volatile 可以说是 Java 虚拟机提供的最轻量级的同步机制。
volatile 变量只能确保 可见性 和 有序性(指令禁重排),不能确保原子性。
volatile 变量通常用作某个操作完成,发生中断或者状态的标志。
当且仅当满足以下条件时,才应该使用 volatile 变量
- 对变量的写入操作不依赖变量的当前值或者可以保证只有单个线程更新变量的值。
- 改变量不会与其它状态变量一起纳入不变性条件中
- 在访问变量时不需要加锁
可见性:一个线程修改了这个变量,其它线程都是可以立即得知的。
案例
public class VisibilityTest {// 可见性测试volatile static boolean flag = false ;public static void main(String[] args) {Thread.yield(); // 阻塞当前线程,直到 thread执行完成new Thread( ()->{flag = true; // 修改为真}).start();// 等待修改线程结束while (Thread.activeCount() > 1)Thread.yeild();System.out.println("After changed");while (flag){// 空旋,如果 thread的修改 可见// 那么程序将一直 空旋}}
}
可以看到 thread 线程将 flag 修改为 true之后,main线程一直在运行,说明thread对volatile变量flag的修改,main线程可见。
原子性: 是指 一个操作是不可中断的,要么全部执行成功要么全部执行失败。
public class AtomicityTest {private volatile static int num1 = 0;private static int num2 = 0;public static void increment1(){// num++ 操作并不是一个原子性的操作// num++ 是一个“读取-修改-写入”的操作序列,并且状态结果依赖于之前的状态num1++;}// 加锁的自增方法public synchronized static void increment2(){num2++;}public static void main(String[] args) {// 启动 100个线程,每个线程对 num修改100次,理论上 num1 和 num2 都应该 为 10000for (int i = 0; i < 100 ; i++) {new Thread( ()->{for (int j = 0; j < 100; j++) {increment1();increment2();}}).start();}// 等待所有累加线程结束while (Thread.activeCount() > 1)Thread.yeild();System.out.println("num1 = " + num1);System.out.println("num2 = " + num2);}}
发现 num1并不是 10000,说明 volatile 并不能保证变量的原子性
使用Javap反编译 increment1的代码,可以看到num++
操作在Class文件中是由 4 条字节码构成的(return指令并不是由 num++产生的),从字节码层面就可很容易分析出并发失败的原因:当 getstatic 指令把 num的值取到操作数栈的栈顶时,volatile关键字保证了num的值是正确的,但是在执行 iconst_1,iadd指令的时候,其它线程可能将 num的值修改了,此时操作数栈顶的值就变成了过期的数据,所以 putstatic 指令执行过后就可能将较小的num值同步回主内存中。
由于 volatile变量只能保证可见性,当在不符合以下两条规则的运算场景中,仍然需要通过加锁来保证原子性
(synchronized、java.util.concurrent中的锁或者原子类)来保证原子性:
- 运算结果不依赖变量当前值,或者能够保证只有单一的线程对变量进行修改
- 变量不与其它状态变量一起纳入不变性条件中
重排序是指编译器和处理器为了优化程序性能而对指令序列进行重新排序的一种手段,有时候会改变程序语句的先后顺序不存在数据依赖关系,可以重排序;存在数据依赖关系,禁止重排序,并且重排后的指令绝对不能改变原有的串行语义!
1. 什么是指令重排序
禁止指令重排序
是 volatile 变量的第二个作用,普通的变量只能保证在该方法执行过程中所有依赖赋值结果的地方都能获取到正确的结果,而不能保证变量赋值操作的顺序与代码中的执行顺序一致。
int x = 1; ①
int y = 2; ②
x = x + 1; ③
y = 3 * x; ④
// 我们想要的执行顺序是 ①②③④,但实际执行顺序可能是 ②①③④
重排序的分类和执行流程
重排序的分类和执行流程
编译器优化的重排序:编译器在不改变单线程串行语义的前提下,可以重新调整指令的执行顺序
指令级并行的重排序:处理器使用指令级并行技术来讲多条指令重叠执行,若不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺内存
系统的重排序:由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是乱序执行。
数据依赖性:若两个操作访问同一变量,且这两个操作中有一个为写操作,此时两操作间就存在数据依赖性。
2. 指令重拍可能造成的影响
在单线程中,由于 线程内表现为串行
,所以始终能确保最终执行结果和代码顺序的结果一致 ,所以不用担心指令重排会导致结果不正确;
下面三种情况,只要改变两个操作的执行顺序,程序的执行结果就会改变
案例
public class ReSortTest {volatile static int num; // 使用volatile 禁止指令重排序volatile static boolean flag; // 保证可见性public static void set(){num = 6;flag = true;}public static void print(){while (flag) {num = num + 1;System.out.println("num = " + num);}}
}
// 如果允许指令重排序,就有可能出现,flag=true了(还没执行num = 6),就执行 num = num + 1;输出的就可能是 num = 1
volatile 底层是通过内存屏障来实现有序性和可见性的
volatile读原理
volatile写原理
== 总结==
部分内容摘抄至《深入理解Java虚拟机》。