在Java并发编程中volatile关键字是一个非常重要的工具,使用得好能够极大提高程序的性能。理解volatile是使用它的前提。否则程序运行的结果就不能达到预期。为了说清除volatile关键字就不得不提两个东西,一个是JAVA内存抽象模型,二个就是JVM指令重排序。
JAVA内存模型
JAVA线程之间的通信由Java内存模型控制(JMM)。JMM定义了线程对共享变量的写入何时对另外一个线程可见。从抽象的角度JMM定义了主内存用于存放共享变量。同时为每一个线程定义了一个本地内存。本地内存用于存放共享变量的副本。本地内存是一个高速缓存区。用于解决CPU执行指令与内存读写速度不一致的问题。下面是JAVA内存模型抽象图。
基于这个模型当我们写一个变量时。实际上执行了两个操作,一步是写这个变量的缓存区即这个线程的本地内存。然后将缓冲区的数据刷新到主内存。这两个操作不是原子操作。也就是说当一个线程写一个变量的值,当执行完第一步改变本地缓存的值。而这时正好有一个线程读这个变量的值。那么后面这个线程读到的值是一个主内存的旧值。
jmm这样设计的原因是内存读写的速度远远低于cup的运算速度为了匹配cup的速度。内存数据会批量写入道高速缓存区中。同样为了提高速度在写主内存时不会改变一个变量就马上刷新到主内存。而是到适当的时机批量刷新到主内存,这个操作由JMM控制。
而当变量被volatile关键字修饰后就读写该变量就变成了原子操作。
指令重排序
因为现在处理器都是多核结构为了充分利用处理器这一特性。编译器和处理器会对代码中执行前后顺序改变后不会影响运行结果的代码进行重排序以提高代码的执行效率。上面提到的不影响运行结构只是保证单线程环境。这个在多线程环境下可能会出现问题。例如如下代码:
public class Example {
private int a=0;
private boolean flag=false;
public void writer() {
a=1;
flag=true;
}
public void read() {
if(flag) {
int i=a*a;
}
}
}
如果线程A执行Example类对象的writer()方法同时线程B执行同一个对象的read()方法。正常情况下我们会认为要么不执行int i=a*a这个代码,要么执行int i=a*a并且a=1。因为我们认为当flag=true;时a=1已经执行。但是因为指令重排序存在会导致结构不满足我们的预期。这时我们应该i如何处理这个代码使其满足我们的预期。这时volatile关键字就派上用场了。只需要在flag变量上加上volatile关键字。就可以使我们代码满足预期运行。其原因就是volatile可以影响代码重排序。volatile关键字对重排序的影响规则可以总结为两点
1.对volatile变量的写之前的操作不会被重排序到该变量写之后
2.对volatile变量的读之后的操作不会被重排序到该变量读之前
总结
1.volatile关键字会影响JVM后期优化的指令重排序操作
2.volatile关键字保证了被修饰对象的原子读写操作。
说了这么多不结合实际例子说明一两个问题,那么前面扯这么多蛋有什么用呢?接下来举一个单例模式的例子先看下面代码。
public class SingletonInstance {
public static SingletonInstance instance;
public static SingletonInstance getInstance(){
if(null==instance){
instance=new SingletonInstance();
}
return instance;
}
}
很明显这是一个典型的单利模式的例子。但是该例子在多线程环境下不是线程安全的并不能保证只有一个实例。为了保证线程安全我们的代码会改成如下模式:
public class SingletonInstance {
public static SingletonInstance instance;
public static synchronized SingletonInstance getInstance(){
if(null==instance){
instance=new SingletonInstance();
}
return instance;
}
}
加上同步锁后确实能够保证我们的预期。但是当线程少的时候获取单利没有问题。当线程数很多的情况下获取单利这个操作会引起锁竞争影响程序性能。于是有一种聪明的写法出现了。代码如下:
public class SingletonInstance {
public static SingletonInstance instance;
public static SingletonInstance getInstance(){
if(null==instance){
synchronized(SingletonInstance.class){
if(null==instance){
instance=new SingletonInstance();
}
}
}
return instance;
}
}
这乍一看好像是一个完美的方案。实际上我们拿到的对象可能是个没有完全实例化的对象。因为有指令重排序问题。为了分析这个问题,来看一下对象实例化的伪代码
memory=allocate(); //1分配对象内存
ctorInstance(memory); //2初始化对象
instance=memory; //3设置instance的内存地址
因为存在指令重排序1、3很可能被重排序。所以一个简单实用的解决办法就是用volatile关键字修饰instance。因为volatile能够保证改变两的读写原子性。所以最终的结果为:
public class SingletonInstance {
public static volatile SingletonInstance instance;
public static SingletonInstance getInstance(){
if(null==instance){
synchronized(SingletonInstance.class){
if(null==instance){
instance=new SingletonInstance();
}
}
}
return instance;
}
}