java volatile关键字的理解

转载:http://shmilyaw-hotmail-com.iteye.com/blog/1672779

一个多线程的示例引发的问题

在讨论这个关键字之前先看一个多线程的示例代码:

 

Java代码  收藏代码
  1. public class RaceCondition {              
  2.   private static boolean done;  
  3.     
  4.   public static void main(final String[] args) throws InterruptedException{  
  5.     new Thread(  
  6.       new Runnable() {  
  7.         public void run() {  
  8.       int i = 0;  
  9.           while(!done) { i++; }  
  10.             System.out.println("Done!");  
  11.           }  
  12.         }  
  13.     ).start();  
  14.   
  15.     System.out.println("OS: " + System.getProperty("os.name"));    
  16.     Thread.sleep(2000);  
  17.     done = true;  
  18.     System.out.println("flag done set to true");  
  19.   }  
  20. }  

 

这部分代码主要是设置了一个static变量done。main函数的主线程会打印一些必要的信息之后修改该变量的值。而另外一个派生的线程则一直在读取done的信息,根据信息来判断下一步的行为。总的来说就是一个线程等另一个线程修改的数值结果。

如果运行这一段代码,会是什么结果呢?

下面是在我的具体执行环境下的情况:

 

Java代码  收藏代码
  1. OS: Linux  
  2. flag done set to true  

比较有意思的就是代码执行到这里的时候并没有完全退出来,只是一直停在这里。 

从代码的字面含义来看,当main函数主线程将done设置为true的时候,派生的线程应该读取到这个值然后跳出循环的啊,为什么没有跳出来呢?

先别急,如果我们换一种方式来执行上面的代码试试,就会发现不一样的结果了:

如果我们输入如下的命令:

 

Java代码  收藏代码
  1. java -d32 RaceCondition  

这次执行的结果会是:

 

Java代码  收藏代码
  1. OS: Linux  
  2. flag done set to true  
  3. Done!  

这么看来,实在是太诡异了。到底是怎么回事呢?

 

第一步分析

        实际上,首先这个问题就在于我们执行代码的时候所采用的执行方式。java的命令执行模式是和平台相关的。当我们在linux平台用java RaceCondition的时候,java默认采用的是server模式。而后面用java -d32 RaceCondition,就是手动的指示采用client模式来执行。这么说来问题就出在执行模式的差别。

        确实,server模式和client模式执行java代码会有一些差别。server模式会jit的时候对代码做一些优化。更进一步来说,我们前面的问题就在于server模式的优化。为什么这么一优化之后结果就不对了呢?我们可以看下面jvm的结构图来做下一步分析。

jvm

        上面图中,每个java线程都有一套自己独立的栈、指令寄存器、缓存等线程本地存储空间。这样,每次线程执行的时候,一些线程本地的变量或者传入的参数可以在线程内部存储空间处理。而这个问题的关键也在于线程的本地存储空间。在对前面的代码进行优化之后,线程读取到done变量会读取一个副本到本地的存储空间。这样以后每次线程访问这个变量的时候,不会跑到原来定义该变量的内存中来读取,而是直接读取自身的那个副本。这样,我们才会看到第一种方式的执行不会结束。而前面我们在client模式下看到的结果是因为没有这些优化,每次还是从done变量的内存中来读取。

        那么,如果要解决上面那个问题,有哪些办法呢?

一种选项,volatile

        如果说为了结果这样一个问题,我们可以有好几种选项,比如说将done声明为原子数据类型,或者采用synchronized方式来访问它。我们这里可以考虑一下volatile这种方式。

        volatile表示它告诉jit编译器,不要对所修饰的变量进行任何优化。这样,每次每个线程访问修饰的变量时,每次都是访问内存中这个独一无二的变量,不会有其他的本地拷贝。

        volatile提供唯一的内存访问地址容易让人产生一些误解。觉得volatile变量看起来可以实现多线程的安全访问。实际未必。

volatile不保证多个线程访问的原子性

        比如说我们有多个线程要访问一个网站的计数器,假设该变量为count。那么每个对该变量进行一次递增的代码是count++;粗粗看来用volatile应该可以满足了。实际上会有问题。

        我们对count递增的操作实际的执行细节里是细分成了三个步骤。1.读取count,2.递增count 3.将修改后的数值写会内存。 问题就在于,当有多个线程访问的时候,会出现竞争条件,可能导致数据错误。

 

volatile也不能保证线程的互斥访问

       和synchronized的关键字不一样,volatile对于访问变量没有严格限制。所以可以同时有多个线程进行读写操作。这样就不能保证线程安全的。

 

性能方面

        既然volatile修饰的变量就是放在内存中,所以每次每个线程访问的时候都要来访问内存。这样和直接访问寄存器或者缓存比起来要慢不少。如果有大量的线程要访问某些变量,都要去访问内存的话。会带来性能方面的影响。在实际的计算机体系结构中,对于volatile变量的读取性能已经和非volatile变量的读取非常接近,几乎可以忽略了。只有对volatile的写操作会相对慢一些。

 

volatile一些应用的场景

        看了前面的分析,让人觉得有点沮丧。似乎这东西没什么用。从前面对性能的分析,我们可以看到一个应用。那就是如果只有一个线程进行数据的写,大部分的线程只是都数据的话,volatile是一个不错的选项。包括前面的那个简单的示例,如果只是一个普通变量的访问,没有特殊要求,用volatile是一种很简便的解决方法。

        和用synchronized等线程同步机制来限制代码,volatile可以用一种很简单的方式来满足一些多线程访问需求。

对于volatile更多详细的应用可以参考这篇文章.

   应用场景推荐:

   变量的值不依赖于以前的值:比如I++这种操作

   作为状态标志:比如boolean类型的变量

   在ReentrantLock中的使用volatile变量在表示状态

 

总结

        Volatile变量是一种可以在某种情况下简化多线程编程的手法。它限制了多线程访问的jit优化,在某些对性能要求比较高的情况下需要慎重考虑。

posted @ 2016-08-13 22:18  googlemeoften  阅读(209)  评论(0编辑  收藏  举报