一、不得不提的volatile
volatile是个很老的关键字,几乎伴随着JDK的诞生而诞生,我们都知道这个关键字,但又不太清楚什么时候会使用它;我们在JDK及开源框架中随处可见这个关键字,但并发专家又往往建议我们远离它。比如Thread这个很基础的类,其中很重要的线程状态字段,就是用volatile来修饰,见代码
/* Java thread status for tools,
* initialized to indicate thread 'not yet started'
*/
private volatile int threadStatus = 0;
如上面所说,并发专家建议我们远离它,尤其是在JDK6的synchronized关键字的性能被大幅优化之后,更是几乎没有使用它的场景,但这仍然是个值得研究的关键字,研究它的意义不在于去使用它,而在于理解它对理解Java的整个多线程的机制是很有帮助的。
1. 例子
先来体会一下volatile的作用,从下面代码开始
1: public class VolatileExample extends Thread{
2: //设置类静态变量,各线程访问这同一共享变量
3: private static boolean flag = false;
4:
5: //无限循环,等待flag变为true时才跳出循环
6: public void run() {while (!flag){};}
7:
8: public static void main(String[] args) throws Exception {
9: new VolatileExample().start();
10: //sleep的目的是等待线程启动完毕,也就是说进入run的无限循环体了
11: Thread.sleep(100);
12: flag = true;
13: }
14: }
这个例子很好理解,main函数里启动一个线程,其run方法是一个以flag为标志位的无限循环。如果flag为true则跳出循环。当main执行到12行的时候,flag被置为true,按逻辑分析此时线程该结束,即整个程序执行完毕。
执行一下看看是什么结果?结果是令人惊讶的,程序始终也不会结束。main是肯定结束了的,其原因就是线程的run方法未结束,即run方法中的flag仍然为false。
把第3行加上volatile修饰符,即
private static volatile boolean flag = false;
再执行一遍看看?结果是程序正常退出,volatile生效了。
我们再修改一下。去掉volatile关键字,恢复到起始的例子,然后把while(!flag){}改为while(!flag){System.out.println(1);},再执行一下看看。按分析,没有volatile关键字的时候,程序不会执行结束,虽然加上了打印语句,但没有做任何的关键字/逻辑的修改,应该程序也不会结束才对,但执行结果却是:程序正常结束。
有了这些感性认识,我们再来分析volatile的语义以及它的作用。
2.volatile语义
volatile的第一条语义是保证线程间变量的可见性,简单地说就是当线程A对变量X进行了修改后,在线程A后面执行的其他线程能看到变量X的变动,更详细地说是要符合以下两个规则:
线程对变量进行修改之后,要立刻回写到主内存。
线程对变量读取的时候,要从主内存中读,而不是缓存。
要详细地解释这个问题,就不得不提一下Java的内存模型(Java Memory Model,简称JMM)。Java的内存模型是一个比较复杂的话题,属于Java语言规范的范畴,个人水平有限,不能在有限篇幅里完整地讲述清楚这个事,如果要清晰地认识,请学习《深入理解Java虚拟机-JVM高级特性与最佳实践》和《The Java Language Specification, Java SE 7 Edition》,这里简单地引用一些资料略加解释。
Java为了保证其平台性,使Java应用程序与操作系统内存模型隔离开,需要定义自己的内存模型。在Java内存模型中,内存分为主内存和工作内存两个部分,其中主内存是所有线程所共享的,而工作内存则是每个线程分配一份,各线程的工作内存间彼此独立、互不可见,在线程启动的时候,虚拟机为每个内存分配一块工作内存,不仅包含了线程内部定义的局部变量,也包含了线程所需要使用的共享变量(非线程内构造的对象)的副本,即为了提高执行效率,读取副本比直接读取主内存更快(这里可以简单地将主内存理解为虚拟机中的堆,而工作内存理解为栈(或称为虚拟机栈),栈是连续的小空间、顺序入栈出栈,而堆是不连续的大空间,所以在栈中寻址的速度比堆要快很多)。工作内存与主内存之间的数据 交换 通过主内存来进行,如下图:同时,Java内存模型还定义了一系列工作内存和主内存之间交互的操作及操作之间的顺序的规则(这规则比较多也比较复杂,参见《深入理解Java虚拟机-JVM高级特性与最佳实践》第12章12.3.2部分),这里只谈和volatile有关的部分。对于共享普通变量来说,约定了变量在工作内存中发生变化了之后,必须要回写到主内存(迟早要回写但并非马上回写),但对于volatile变量则要求工作内存中发生变化之后,必须马上回写到住主内存,而线程读取volatile变量的时候,必须马上到主内存中去取最新值而不是读取本地工作内存的副本,此规则保证了前面所说的“当线程A对变量X进行了修改后,在线程A后面执行的其他线程能看到变量X的变动”。
大部分网上的文章对于volatile的解释都是到此为止,但我觉得还是有遗漏的,提出来探讨。工作内存可以说是主内存的一份缓存,为了避免缓存的不一致性,所以volatile需要废弃此缓存。但除了内存缓存之外,在CPU硬件级别也是有缓存的,即寄存器。假如线程A将变量X由0修改为1的时候,CPU是在其缓存内操作,没有及时回写到内存,那么JVM是无法X=1是能及时被之后执行的线程B看到的,所以我觉得JVM在处理volatile变量的时候,也同样用了硬件级别的缓存一致性原则(CPU的缓存一致性原则参见《Java的多线程机制系列:(二)缓存一致性和CAS》。
volatile的第二条语义:禁止指令重排序。关于指令重排序请参见后面的“指令重排序”章节。这是volatile目前主要的一个使用场景。