java volatile关键字解析
volatile是什么
volatile在java语言中是一个关键字,用于修饰变量。被volatile修饰的变量后,表示这个变量在不同线程中是共享,编译器与运行时都会注意到这个变量是共享的,因此不会对该变量进行重排序。上面这句话可能不好理解,但是存在两个关键,共享和重排序。
变量的共享
先来看一个被举烂了的例子:
1 public class VolatileTest { 2 3 boolean isStop = false; 4 5 public void test() { 6 Thread t1 = new Thread() { 7 @Override 8 public void run() { 9 isStop = true; 10 } 11 }; 12 Thread t2 = new Thread() { 13 @Override 14 public void run() { 15 while (!isStop) { 16 } 17 } 18 }; 19 t2.start(); 20 t1.start(); 21 } 22 23 public static void main(String args[]) throws InterruptedException { 24 new VolatileTest().test(); 25 } 26 }
(注:线程2中,while内容里如果写个System.out.prientln(""),导致循环退出,目前没明白什么原因。)
上面的代码是一种典型用法,检查某个标记(isStop)的状态判断是否退出循环。但是上面的代码有可能会结束,也可能永远不会结束。因为每一个线程都拥有自己的工作内存,当一个线程读取变量的时候,会把变量在自己内存中拷贝一份。之后访问该变量的时候都通过访问线程的工作内存,如果修改该变量,则将工作内存中的变量修改,然后再更新到主存上。这种机制让程序可以更快的运行,然而也会遇到像上述例子这样的情况。
存在一种情况,isStop变量被分别拷贝到t1、t2两个线程中,此时isStop为false。t2开始循环,t1修改本地isStop变量称为true,并将isStop=true回写到主存,但是isStop已经在t2线程中拷贝过一份,t2循环时候读取的是t2 工作内存中的isStop变量,而这个isStop始终是false,程序死循环。我们称t2对t1更新isStop变量的行为是不可见的。
如果isStop变量通过volatile进行修饰,t2修改isStop变量后,会立即将变量回写到主存中,并将t1里的isStop失效。t1发现自己变量失效后,会重新去主存中访问isStop变量,而此时的isStop变量已经变成true。循环退出。
volatile boolean isStop = false;
代码的重排序
再来看一个被举烂了的例子:
1 //线程1: 2 context = loadContext(); //语句1 3 inited = true; //语句2 4 5 //线程2: 6 while(!inited ){ 7 sleep() 8 } 9 doSomethingwithconfig(context);
(注:感觉很难模拟,我没能模拟出来,也没找到他人的模拟结果)
如上代码示例,按照正常的想法,context初始化后,再把inited赋值为true。但是有可能有语句2先执行,再执行语句1的情况。导致线程2中doSomeThingWithConfig报错。因为jvm对代码进行编译的时候会进行指令优化,调整互不关联的两行代码执行顺序,在单线程的时候,指令优化会保证优化后的结果不会出错。但是在多线程的时候,可能发生像上述例子里的问题。如果上述的inited用volatile修饰,就不会有问题。
《深入理解Java虚拟机》中有一句话:“观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令”,lock前缀指令生成一个内存屏障。保证重排序后的指令不会越过内存屏障,即volatile之前的代码只会在volatile之前执行,volaiter之后的代码只会在volatile之后执行。
volatile怎么用
volatile关键字一般用于标记变量的修饰,类似上述例子。《Java并发编程实战》中说,volatile只保证可见性,而加锁机制既可以确保可见性又可以确保原子性。当且仅当满足以下条件下,才应该使用volatile变量:
1、对变量的写入操作不依赖变量的当前值,或者确保只有单个线程变更变量的值。
2、该变量不会于其他状态一起纳入不变性条件中
3、在访问变量的时候不需要加锁。
逐一分析:
第一条说明volatile不能作为多线程中的计数器,计数器的count++操作,分为三步,第一步先读取count的数值,第二步count+1,第三步将count+1的结果写入count。volatile不能保证操作的原子性。上述的三步操作中,如果有其他线程对count进行操作,就可能导致数据出错。
第二条:
1 public class VolatileTest { 2 3 4 private volatile int lower = 0; 5 private volatile int upper = 5; 6 7 public int getLower() { 8 return lower; 9 } 10 11 public int getUpper() { 12 return upper; 13 } 14 15 public void setLower(int lower) { 16 if (lower > upper) { 17 return; 18 } 19 this.lower = lower; 20 } 21 22 public void setUpper(int upper) { 23 if (upper < lower) { 24 return; 25 } 26 this.upper = upper; 27 } 28 }
上述程序中,lower初始为0,upper初始为5,并且upper和lower都用volatile修饰。我们期望不管怎么修改upper或者lower,都能保证upper>lower恒成立。然而如果同时有两个线程,t1调用setLower,t2调用setUpper,两线程同时执行的时候。有可能会产生upper<lower这种不期望的结果。
测试代码:
1 public void test() { 2 Thread t1 = new Thread() { 3 @Override 4 public void run() { 5 try { 6 Thread.sleep(10); 7 } catch (InterruptedException e) { 8 e.printStackTrace(); 9 } 10 setLower(4); 11 } 12 }; 13 Thread t2 = new Thread() { 14 @Override 15 public void run() { 16 try { 17 Thread.sleep(10); 18 } catch (InterruptedException e) { 19 e.printStackTrace(); 20 } 21 setUpper(3); 22 } 23 }; 24 25 t1.start(); 26 t2.start(); 27 28 while (t1.isAlive() || t2.isAlive()) { 29 30 } 31 System.out.println("(low:" + getLower() + ",upper:" + getUpper() + ")"); 32 33 } 34 35 public static void main(String args[]) throws InterruptedException { 36 for (int i = 0; i < 100; i++) { 37 VolatileTest volaitil = new VolatileTest(); 38 volaitil.test(); 39 } 40 }
输出结果:
此时程序一直正常运行,但是出现的结果却是我们不想要的。
第三条:当访问一个变量需要加锁时,一般认为这个变量需要保证原子性和可见性,而volatile关键字只能保证变量的可见性,无法保证原子性。
最后贴个volatile的常见例子,在单例模式双重检查中的使用:
1 public class Singleton { 2 3 private static volatile Singleton instance=null; 4 5 private Singleton(){ 6 } 7 8 public static Singleton getInstance(){ 9 if(instance==null){ 10 synchronized(Singleton.class){ 11 if(instance==null){ 12 instance=new Singleton(); 13 } 14 } 15 } 16 return instance; 17 } 18 19 }
new Singleton()分为三步,1、分配内存空间,2、初始化对象,3、设置instance指向被分配的地址。然而指令的重新排序,可能优化指令为1、3、2的顺序。如果是单个线程访问,不会有任何问题。但是如果两个线程同时获取getInstance,其中一个线程执行完1和3步骤,此时其他的线程可以获取到instance的地址,在进行if(instance==null)时,判断出来的结果为false,导致其他线程直接获取到了一个未进行初始化的instance,这可能导致程序的出错。所以用volatile修饰instance,禁止指令的重排序,保证程序能正常运行。(Bug很难出现,没能模拟出来)。
然而,《java并发编程实战中》中有对DCL的描述如下:"DCL的这种使用方法已经被广泛废弃了——促使该模式出现的驱动力(无竞争同步的执行速度很慢,以及JVM启动很慢)已经不复存在了,因而它不是一种高效的优化措施。延迟初始化占位类模式能带来同样的优势,并且更容易理解。",其实我个小码畜的角度来看,服务端的单例更多时候做延迟初始化并没有很大意义,延迟初始化一般用来针对高开销的操作,并且被延迟初始化的对象都是不需要马上使用到的。然而,服务端的单例在大部分的时候,被设计为单例的类大部分都会被系统很快访问到。本篇文章只是讨论volatile,并不针对设计模式进行讨论,因此后续有时间,再补上替代上述单例的写法。