前言
volatile在Java中是一个变量修饰符,其作用是保证变量在线程之间的可见性以及防止指令重排序。
1 保证线程可见性
关于线程之间的可见性,在之前的文章《并发编程之可见性》中已经详细描述过,所以这里不再赘述。
2 防止指令重排序
2.1 什么是指令重排序
为了提升指令的执行效率,编译器会对代码结构进行重新排序,达到最佳效果。比如下面的代码:
int x = 1;
int y = 2;
经过编译器优化之后,其执行顺序可能会变成:
int y = 2;
int x = 1;
这种优化对于最终的结果来说并没有什么影响,保证了最终一致性。而现代处理器为了提升其指令执行的效率,采用了指令级并行技术(Instruction-LevelParallelism,ILP)来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。
2.2 指令重排序可能造成的问题
在单线程环境下,指令重排序并不会产生数据不一致的问题。但是在多线程的情况下,问题就出现了。下面借助双重检锁(DCL)单例模式来说明这个问题。
public class Singleton {
private static Singleton instance;
private int num;
private Singleton() {
num = 1;
}
public int getNum() {
return num;
}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
上面是双重检锁单例模式的代码,将其编译成字节码之后,instance = new Singleton()对应的字节码为:
17 new #4 <cn/spc/test/Singleton>
20 dup
21 invokespecial #5 <cn/spc/test/Singleton.<init>>
24 putstatic #3 <cn/spc/test/Singleton.instance>
27 aload_0
从上面的字节码可以看出,instance = new Singleton()主要有三个步骤:
- new #4 <cn/spc/test/Singleton>为对象分配空间,此时成员变量num的值为0。
- invokespecial #5 <cn/spc/test/Singleton.>调用构造方法,此时成员变量num的值为1。
- aload_0将对象的地址赋值给instance变量。
在上面的过程中,如果步骤2和步骤3发生了指令重排序,问题就出现了。当第一个线程尝试获取Singleton实例,因为instance是null,所以会创建Singleton实例。假设这个线程在创建Singleton实例执行时,先执行了步骤1,再执行了步骤3,最后执行了步骤2。对于该线程来说,它最终得到了一个正确的Singleton实例。但是,如果有另外一个线程,在第一个线程执行到步骤3的时候,尝试获取Singleton实例,因为这个时候已经将对象的地址赋值给了instance变量,所以这个线程也可以拿到instance。但是,这个时候第一个线程还没有执行步骤2,所以instance的num成员变量值为0而不是1,这意味着这个线程拿到了一个中间状态的Singleton实例。
2.3 使用volatile防止指令重排序
为了解决上面的问题,只需要用volatile修饰成员变量instance即可。
public class Singleton {
private static volatile Singleton instance;
private int num;
private Singleton() {
num = 1;
}
public int getNum() {
return num;
}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
2.4 volatile防止指令重排序的原理
volatile是通过内存屏障来禁止指令重排序的,内存屏障前后两条指令不能进行重排序。JVM内存屏障策略为:
- 在每个volatile写操作的前面插入一个StoreStore屏障。
- 在每个volatile写操作的后面插入一个StoreLoad屏障。
- 在每个volatile读操作的后面插入一个LoadLoad屏障。
- 在每个volatile读操作的后面插入一个LoadStore屏障。
总结
以上就volatile关于禁止指令重排序的内容,这里只是简单地描述了指令重排序的概念、指令重排序可能造成的问题、解决方法以及volatile禁止重排序的原理。关于指令重排序,其实还有很多更加深入的内容,后续有机会将会继续讲解。