- 基础概念 -
java内存模型允许编译器和处理器对指令重排,目的减少流水线的中断,从而提高流水线运行效率。
数据依赖不会重排序。在单线程下重排序可以保证最终执行的结果与程序顺序执行结果一致。
as-if-serial (仿佛是序列) 不管怎么样重排序,单线程程序的执行结果不能被改变,编译器,runtime 和处理器必须遵守 as-if-serial 语义
- 数据依赖性(不会重排序) -
如果两个操作访问同一个变量,且这两个操作有一个写操作,两个操作之间存在数据依赖性
一. 重排序现象
private static Singleton singleton;
public static Singleton getInstance() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
复制
给 singleton 分配堆内存(Single 对象)。
调用 Single 的构造函数来初始化成员变量,形成实例。
将 singleton 指针指向分配的内存空间(执行完这步 singleton 才是非 null 了)。
正常执行顺序:1->2->3,由于操作2和操作3没有依赖性(操作1和操作3有依赖性),可能发生指令重排,可能的执行顺序为:1->3->2。
回到代码,当操作1,3执行后,singleton 指针是不为 null 了,此时,另一个线程执行 if(singleton == null) 就会不成立,直接返回,而此时,Singleton的构造还可能未执行,会引发严重数据错误。
volatile关键字的一个作用是禁止指令重排,把singleton声明为volatile之后,对它的写操作就会有一个内存屏障,这样在它的赋值完成之前,就不用会调用读操作。
private volatile static Singleton ; // 防止指令重排
复制
public class Test {
static int x = 0, y = 0;
static int a = 0, b = 0;
public static void main(String[] args) throws InterruptedException {
while (true) {
reSort();
}
}
static void reSort() throws InterruptedException {
Thread ta = new Thread(new Runnable() {
public void run() {
a = 1; //操作1
x = b; //操作2
}
});
Thread tb = new Thread(new Runnable() {
public void run() {
b = 1; //操作3
y = a; //操作4
}
});
ta.start();
tb.start();
ta.join();
tb.join();
if (x == 0 && y == 0) {
System.out.println("(" + x + "," + y + ")");
}
x = 0; y = 0; a = 0; b = 0;
}
}
复制
处理器使用写缓存临时保存内存写入数据。写缓存区可以保证指令流水线持续运行,他可以避免由于处理器停顿下来等待想内存写入数据而产生延迟,同时通过以批处理的方式刷新缓存区,以及合并写缓存区对同一内存地址多次写,减少对内存总线的占用。虽然写缓存区有这么多好处,但每个处理器上的写缓存区仅仅对它所在的处理器可见。
处理器A和处理器B 同时把共享变量写入缓存区A1,B1, 然后从内存中读取另一个共享变量(A2,B2),最后才把自己写缓存区中保存的脏数据刷新到内存数据A3,B3 。
1、写 a=1,b=1;
2、读a=0, b=0;
3、刷新
复制
虽然处理器A执行内存操作的顺序为A1->A2, 内存操作世界发生顺序A2->A1。
由于写缓存区仅对自己处理器可见,会导致处理器操作顺序和内存实际执行顺序不一致,由于现在处理器会使用写缓存,因此先到处理器都允许对写-读操作进行重排序
常见的处理器都允许store-load重排序,都不允许对存在数据依赖的操作做重排序,sparc-TSO 和 X86拥有相对较强的处理器内存模型,仅允许对写-读操作做重排序(它们都是用写缓冲区)
- 内存屏障 -
内存屏障类型
编译器和处理器-猜测执行
public class Recorder {
public int m = 0;
private boolean flag = false;
/**
* 操作1 和 操作2 没有依赖关系
*/
public void writer() {
// 操作1
this.m = 1;
// 操作2
this.flag = true;
}
/**
* 1、操作3和操作4存在控制依赖关系,会影响指令序列执行的并行度
* 2、编译器和处理器会采取猜测(Speculation) 执行克服控制相关性对并行度影响
* 3、以处理器的猜测执行为例,执行线程B的处理器可以提前读取并计算a*a,
* 并把计算记过临时保存到一个名为重排序缓存的硬件缓存中,当操作3的条件判断为真时,就把该计算结果写入变量中
*/
public void reader() {
// 操作3
if (flag) {
// 操作4
m = m * m;
}
}
}
复制
- 作者介绍 -
架构师一枚,现就职于小米小爱开放平台,一个才貌双全的美女码农,平常喜欢总结知识和刷算法题,经常参加LeetCode算法周赛。