JAVA 基础系列之 重排序和Volatile

重排序

在执行程序时,编译器和处理器会对指令进行重排序,重排序分为:

  1. 编译器重排序:在不改变代码语义的情况下,优化性能而改变了代码执行顺序;
  2. 指令并行的重排序:处理器采用并行技术使多条指令重叠执行,在不存在数据依赖的情况下,改变机器指令的执行顺序;
  3. 内存系统的重排序:使用缓存和读写缓冲区时,加载和存储可能是乱序执行。

比如现在有一段代码如下:

a = 1; //代码1
b = 1; //代码2

编译器和处理为了提高并行度,可以将代码1和2调整顺序,即先执行代码1和代码2。
但是若是其他情况:

a = 1; //代码3
b = a; //代码4

这种情况因为代码3和4存在数据依赖,存在hanpens-before关系,处理器和编译器会遵守 as-if-serial原则,不会调整顺序。

as-if-serial原则:不可以调整会导致结果改变的代码顺序(仅单线程)。

hanpens-before:指前一个操作对后一个操作可见,并不是前一个操作必须在后一个操作之前执行。

当存在控制依赖时,编译器和处理器会采取猜测执行机制来提高并行度,如下代码:

a = 1;
flag = true ;
if(flag){ //代码5
    a * = 2; //代码6
}

代码5和6不存在数据依赖,可能会重排,处理器和编译器会先将代码6的执行结果放在缓冲区,等执行代码5之后,将缓冲区的结果直接赋值给a。

若要限制重排序,可以使用volatile关键字修饰变量。

volatile限制重排序

volatile会在读的前后加入LoadLoad屏障和LoadStore屏障,在写的前后加入StoreStore和StoreLoad屏障。如下示意图:
这里写图片描述
这里写图片描述

Volatile的重排规则表如下:

这里写图片描述

小结:

  1. 当第一个操作是Volatile读时,不管第二个操作是什么,都不能重排序;
  2. 当第一个操作是Volatile写时,第二个操作是Volatile读或写,不能重排序;
  3. 当第一个操作是普通读写,第二个操作是Volatile写时,不能重排序。

Volatile可见性

Volatile有可见性的特点。用Volatile修饰的变量,在多线程情况下,每个线程对该变量的修改会立刻刷新主存中该变量的值,而不是先保存在线程自身的缓存中。使得每个线程读取该变量时,值是最新的。

但是并不代表Volatile具有原子性,因为出现一种情况就是,线程A读取该变量的值,将进行写操作,但还没进行写操作时,被其他线程读取旧值。

Volatile最常见的应用场景

(1)
单例的双重校验

class Singleton{
    private volatile static Singleton instance = null;

    private Singleton() {

    }

    public static Singleton getInstance() {
        if(instance==null) {
            synchronized (Singleton.class) {
                if(instance==null)
                    instance = new Singleton();
            }
        }
        return instance;
    }
}

(2)状态标记

volatile boolean shutdownRequested;

public void shutdown() { shutdownRequested = true; }

public void doWork() { 
    while (!shutdownRequested) { 
        // do stuff
    }
}

(3)Java的线程安全类也使用了Volatile,例如:
java.util.concurrent.atomic,java.util.concurrent包下的类。

参考文献:
1.深入理解Java内存模型 – 程晓明
2.https://www.ibm.com/developerworks/cn/java/j-jtp06197.html

展开阅读全文

没有更多推荐了,返回首页