java volatile关键字解析

volatile是什么

  volatile在java语言中是一个关键字,用于修饰变量。被volatile修饰的变量后,表示这个变量在不一样线程中是共享,编译器与运行时都会注意到这个变量是共享的,所以不会对该变量进行重排序。上面这句话可能很差理解,可是存在两个关键,共享和重排序。java

变量的共享

先来看一个被举烂了的例子:编程

 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变量被分别拷贝到t一、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。循环退出。jvm

  

volatile boolean isStop = false;

 

代码的重排序

再来看一个被举烂了的例子:ide

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以后执行。this

 

  

volatile怎么用

  volatile关键字通常用于标记变量的修饰,相似上述例子。《Java并发编程实战》中说,volatile只保证可见性,而加锁机制既能够确保可见性又能够确保原子性。当且仅当知足如下条件下,才应该使用volatile变量:

一、对变量的写入操做不依赖变量的当前值,或者确保只有单个线程变动变量的值。

二、该变量不会于其余状态一块儿归入不变性条件中

三、在访问变量的时候不须要加锁。

 

  逐一分析:

  第一条说明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()分为三步,一、分配内存空间,二、初始化对象,三、设置instance指向被分配的地址。然而指令的从新排序,可能优化指令为一、三、2的顺序。若是是单个线程访问,不会有任何问题。可是若是两个线程同时获取getInstance,其中一个线程执行完1和3步骤,此时其余的线程能够获取到instance的地址,在进行if(instance==null)时,判断出来的结果为false,致使其余线程直接获取到了一个未进行初始化的instance,这可能致使程序的出错。因此用volatile修饰instance,禁止指令的重排序,保证程序能正常运行。(Bug很难出现,没能模拟出来)。

  然而,《java并发编程实战中》中有对DCL的描述以下:"DCL的这种使用方法已经被普遍废弃了——促使该模式出现的驱动力(无竞争同步的执行速度很慢,以及JVM启动很慢)已经不复存在了,于是它不是一种高效的优化措施。延迟初始化占位类模式能带来一样的优点,而且更容易理解。",其实我个小码畜的角度来看,服务端的单例更多时候作延迟初始化并无很大意义,延迟初始化通常用来针对高开销的操做,而且被延迟初始化的对象都是不须要立刻使用到的。然而,服务端的单例在大部分的时候,被设计为单例的类大部分都会被系统很快访问到。本篇文章只是讨论volatile,并不针对设计模式进行讨论,所以后续有时间,再补上替代上述单例的写法。

 

 

有任何的不合适或者错误的地方还请留言指正。

相关文章
相关标签/搜索