重排序
在执行程序时,编译器和处理器会对指令进行重排序,重排序分为:
- 编译器重排序:在不改变代码语义的情况下,优化性能而改变了代码执行顺序;
- 指令并行的重排序:处理器采用并行技术使多条指令重叠执行,在不存在数据依赖的情况下,改变机器指令的执行顺序;
- 内存系统的重排序:使用缓存和读写缓冲区时,加载和存储可能是乱序执行。
比如现在有一段代码如下:
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的重排规则表如下:
小结:
- 当第一个操作是Volatile读时,不管第二个操作是什么,都不能重排序;
- 当第一个操作是Volatile写时,第二个操作是Volatile读或写,不能重排序;
- 当第一个操作是普通读写,第二个操作是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