• 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优化,在某些对性能要求比较高的情况下需要慎重考虑。

  • 相关阅读:
    Codeforces 220C
    Codeforces 697D
    HDU 4417
    Codeforces 396C
    Codeforces 246C
    HDU 6333
    HDU 3389
    总结:树上启发式合并
    HDU 6319
    Codeforces 1009G
  • 原文地址:https://www.cnblogs.com/googlemeoften/p/5769048.html
Copyright © 2020-2023  润新知