volatile两大作用
  1、保证内存可见性
  2、防止指令重排
  此外需注意volatile并不保证操作的原子性。
  (一)内存可见性
  1 概念
  JVM内存模型:主内存和线程独立的工作内存
  Java内存模型规定,对于多个线程共享的变量,存储在主内存当中,每个线程都有自己独立的工作内存(比如CPU的寄存器),线程只能访问自己的工作内存,不可以访问其它线程的工作内存。
  工作内存中保存了主内存共享变量的副本,线程要操作这些共享变量,只能通过操作工作内存中的副本来实现,操作完毕之后再同步回到主内存当中。
  如何保证多个线程操作主内存的数据完整性是一个难题,Java内存模型也规定了工作内存与主内存之间交互的协议,定义了8种原子操作:
  (1) lock:将主内存中的变量锁定,为一个线程所独占
  (2) unclock:将lock加的锁定解除,此时其它的线程可以有机会访问此变量
  (3) read:将主内存中的变量值读到工作内存当中
  (4) load:将read读取的值保存到工作内存中的变量副本中。
  (5) use:将值传递给线程的代码执行引擎
  (6) assign:将执行引擎处理返回的值重新赋值给变量副本
  (7) store:将变量副本的值存储到主内存中。
  (8) write:将store存储的值写入到主内存的共享变量当中。
  通过上面Java内存模型的概述,我们会注意到这么一个问题,每个线程在获取锁之后会在自己的工作内存来操作共享变量,操作完成之后将工作内存中的副本回写到主内存,并且在其它线程从主内存将变量同步回自己的工作内存之前,共享变量的改变对其是不可见的。即其他线程的本地内存中的变量已经是过时的,并不是更新后的值。
  2 内存可见性带来的问题
  很多时候我们需要一个线程对共享变量的改动,其它线程也需要立即得知这个改动该怎么办呢?下面举两个例子说明内存可见性的重要性:
  例子1
  有一个全局的状态变量open:
  boolean open=true;
  这个变量用来描述对一个资源的打开关闭状态,true表示打开,false表示关闭,假设有一个线程A,在执行一些操作后将open修改为false:
  <strong>//线程A
  resource.close();
  open = false;
  线程B随时关注open的状态,当open为true的时候通过访问资源来进行一些操作:
  <strong>//线程B
  while(open) {
  doSomethingWithResource(resource);
  }
  当A把资源关闭的时候,open变量对线程B是不可见的,如果此时open变量的改动尚未同步到线程B的工作内存中,那么线程B会用一个已经关闭了的资源去做一些操作,因此产生错误。
  例子2
  下面是一个通过布尔标志判断线程是否结束的例子:
public class CancelThreadTest {
publicstatic void main(String[] args) throws Exception{
PrimeGeneratorgen = new PrimeGenerator();
newThread(gen).start();
try
{
Thread.sleep(3000);
}finally{
gen.cancel();
}
}
}
class PrimeGenerator implements Runnable{
privateboolean cancelled;
@Override
publicvoid run() {
while(!cancelled)
{
System.out.println("Running...");
//doingsomething here...
}
}
publicvoid cancel(){cancelled = true;}
}
  主线程中设置PrimeGenerator线程的是否取消标识,PrimeGenerator线程检测到这个标识后会结束线程,由于主线程修改cancelled变量的内存可见性,主线程修改cancelled标识后并不马上同步回主内存,所以PrimeGenerator线程结束的时间难以把控(终是一定会同步回主内存,让PrimeGenerator线程结束)。
  如果PrimeGenerator线程执行一些比较关键的操作,主线程希望能够及时终止它,这时将cenceled用volatile关键字修饰是必要的。
  特别注意:上面演示这个并不是正确的取消线程的方法,因为一旦PrimeGenerator线程中包含BolckingQueue.put()等阻塞方法,那么将可能永远不会去检查cancelled标识,导致线程永远不会退出。正确的方法参见另外一篇关于如何正确终止线程的方法。
  3 提供内存可见性
  volatile保证可见性的原理是在每次访问变量时都会进行一次刷新,因此每次访问都是主内存中新的版本。所以volatile关键字的作用之一是保证变量修改的实时可见性。
  针对上面的例子1:
  要求一个线程对open的改变,其他的线程能够立即可见,Java为此提供了volatile关键字,在声明open变量的时候加入volatile关键字可以保证open的内存可见性,即open的改变对所有的线程都是立即可见的。
  针对上面的例子2:
  将cancelled标志设置的volatile保证主线程针对cancelled标识的修改能够让PrimeGenerator线程立马看到。
  备注:也可以通过提供synchronized同步的open变量的Get/Set方法解决此内存可见性问题,因为要Get变量open,必须等Set方完全释放锁之后。后面将介绍到两者的区别。
  (二)指令重排
  1 概念
  指令重排序是JVM为了优化指令,提高程序运行效率,在不影响单线程程序执行结果的前提下,尽可能地提高并行度。编译器、处理器也遵循这样一个目标。注意是单线程。多线程的情况下指令重排序会给程序员带来问题。
  不同的指令间可能存在数据依赖。比如下面计算圆的面积的语句:
  double r = 2.3d;//(1)
  double pi =3.1415926; //(2)
  double area = pi* r * r; //(3)
  area的计算依赖于r与pi两个变量的赋值指令。而r与pi无依赖关系。
  as-if-serial语义是指:不管如何重排序(编译器与处理器为了提高并行度),(单线程)程序的结果不能被改变。这是编译器、Runtime、处理器必须遵守的语义。
  虽然,(1) – happensbefore -> (2),(2) – happens before -> (3),但是计算顺序(1)(2)(3)与(2)(1)(3) 对于r、pi、area变量的结果并无区别。编译器、Runtime在优化时可以根据情况重排序(1)与(2),而丝毫不影响程序的结果。
  指令重排序包括编译器重排序和运行时重排序。
  2 指令重排带来的问题
  如果一个操作不是原子的,会给JVM留下重排的机会。下面看几个例子:
  例子1:A线程指令重排导致B线程出错
  对于在同一个线程内,这样的改变是不会对逻辑产生影响的,但是在多线程的情况下指令重排序会带来问题。看下面这个情景:
  在线程A中:
  context = loadContext();
  inited = true;
  在线程B中:
  while(!inited ){ //根据线程A中对inited变量的修改决定是否使用context变量
  sleep(100);
  }
  doSomethingwithconfig(context);
  假设线程A中发生了指令重排序:
  inited = true;
  context = loadContext();
  那么B中很可能会拿到一个尚未初始化或尚未初始化完成的context,从而引发程序错误。
  例子2:指令重排导致单例模式失效
  我们都知道一个经典的懒加载方式的双重判断单例模式:
  public class Singleton {
  private static Singleton instance = null;
  private Singleton() { }
  public static Singleton getInstance() {
  if(instance == null) {
  synchronzied(Singleton.class) {
  if(instance == null) {
  <strong>instance = new Singleton();  //非原子操作
  }
  }
  }
  return instance;
  }
  }
  看似简单的一段赋值语句:instance= new Singleton(),但是很不幸它并不是一个原子操作,其实际上可以抽象为下面几条JVM指令:
  memory =allocate();    //1:分配对象的内存空间
  ctorInstance(memory);  //2:初始化对象
  instance =memory;     //3:设置instance指向刚分配的内存地址
  上面操作2依赖于操作1,但是操作3并不依赖于操作2,所以JVM是可以针对它们进行指令的优化重排序的,经过重排序后如下:
  memory =allocate();    //1:分配对象的内存空间
  instance =memory;     //3:instance指向刚分配的内存地址,此时对象还未初始化
  ctorInstance(memory);  //2:初始化对象
  可以看到指令重排之后,instance指向分配好的内存放在了前面,而这段内存的初始化被排在了后面。
  在线程A执行这段赋值语句,在初始化分配对象之前已经将其赋值给instance引用,恰好另一个线程进入方法判断instance引用不为null,然后将其返回使用,导致出错。