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,并不针对设计模式进行讨论,因此后续有时间,再补上替代上述单例的写法。

 

 

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

posted on 2018-03-14 17:56  阿姆斯特朗回旋炮  阅读(3723)  评论(1编辑  收藏  举报

导航