目录
重排序
重排序是编译器和处理器优化程序性能的一种重要方式。
重排序有两种类型:
- 编译期优化的重排序:编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。
- 指令级并行的重排序:如果不存在数据依赖性,处理器可以改变语句对应的机器指令的执行顺序。
PS:请注意重排的是执行顺序。
《单线程程序语义》能够保证重排序不影响单线程程序的执行结果,但是重排序可能改变多线程程序的执行结果。
【例一:编译期优化的重排序】
class RecorderExample{
int a = 0;
boolean flag = false;
public void write(){
a = 1; // 1
flag = true; // 2
}
public void read(){
if(!flag){
a = 2;
}
}
}
理想情况下可能的时序:
a=1 |
|
|
if(!flag) |
|
a=2 |
flag=true |
. |
操作1 与操作2 重排序的情况下,可能的时序:
flag=true |
|
|
if(!flag) |
a=1 |
. |
两种情况结果完全不同。
如何解决?两个方法都用 synchronized 关键字修饰加锁,此时同步块内的重排序并不影响多线程的执行结果。
【例二:指令级并行的重排序】
以懒汉式单例的双重检查锁定为例:
public class Singleton {
private static Singleton instance = null;
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
对象的初始化可以分解为以下伪代码:
memory = allocate(); // 1:分配对象的内存空间
ctorInstance(memory); // 2:初始化对象
instance = memory; // 3:设置 instance 指向内存空间
而编译器很可能为了优化程序的执行效率进行指令重排序,即对 2 和 3 进行互换。
memory = allocate(); // 1:分配对象的内存空间
instance = memory; // 3:设置 instance 指向内存空间(此时对象还没有被初始化!)
ctorInstance(memory); // 2:初始化对象
在重排序的情况下,多线程访问可能会得到尚未初始化的对象。
如何解决?可以使用 volatile 关键字修饰 instance 变量,提供内存屏障禁止指令重排序。
append
【Q】 单线程中会不会得到一个尚未完全初始化的对象呢?
虽然单线程中允许上述重排序,但当使用 instance 引用的时候其指向的对象已经初始化成功了。 来源:http://www./content-4-27131.html
|