转载:http://shmilyaw-hotmail-com.iteye.com/blog/1672779
一个多线程的示例引发的问题
在讨论这个关键字之前先看一个多线程的示例代码:
- public class RaceCondition {
- private static boolean done;
- public static void main(final String[] args) throws InterruptedException{
- new Thread(
- new Runnable() {
- public void run() {
- int i = 0;
- while(!done) { i++; }
- System.out.println("Done!");
- }
- }
- ).start();
- System.out.println("OS: " + System.getProperty("os.name"));
- Thread.sleep(2000);
- done = true;
- System.out.println("flag done set to true");
- }
- }
这部分代码主要是设置了一个static变量done。main函数的主线程会打印一些必要的信息之后修改该变量的值。而另外一个派生的线程则一直在读取done的信息,根据信息来判断下一步的行为。总的来说就是一个线程等另一个线程修改的数值结果。
如果运行这一段代码,会是什么结果呢?
下面是在我的具体执行环境下的情况:
- OS: Linux
- flag done set to true
比较有意思的就是代码执行到这里的时候并没有完全退出来,只是一直停在这里。
从代码的字面含义来看,当main函数主线程将done设置为true的时候,派生的线程应该读取到这个值然后跳出循环的啊,为什么没有跳出来呢?
先别急,如果我们换一种方式来执行上面的代码试试,就会发现不一样的结果了:
如果我们输入如下的命令:
- java -d32 RaceCondition
这次执行的结果会是:
- OS: Linux
- flag done set to true
- Done!
这么看来,实在是太诡异了。到底是怎么回事呢?
第一步分析
实际上,首先这个问题就在于我们执行代码的时候所采用的执行方式。java的命令执行模式是和平台相关的。当我们在linux平台用java RaceCondition的时候,java默认采用的是server模式。而后面用java -d32 RaceCondition,就是手动的指示采用client模式来执行。这么说来问题就出在执行模式的差别。
确实,server模式和client模式执行java代码会有一些差别。server模式会jit的时候对代码做一些优化。更进一步来说,我们前面的问题就在于server模式的优化。为什么这么一优化之后结果就不对了呢?我们可以看下面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优化,在某些对性能要求比较高的情况下需要慎重考虑。