关于volatile关键字的理解
这两天面试,问到了DCL的一些问题,就想起了一个平常写DCL中容易忽略的一个关键字volatile,我们知道volatile是个轻量级的synchronized,他主要在多线程开发中保证了共享变量的 “可见性(当一个线程修改一个共享变量时,另外一个线程能够读到这个修改的值;那么 他是怎么做到保证共享变量的“可见性”的呢?
从java语言规范第三版中对volatile的定义如下:
java编程语言允许线程访问共享变量,为了确保共享变量能被准确和一致的更新,线程应该确保通过排它锁单独获得这个变量。java语言提供了volatile,在某些情况下比锁更加方便。如果一个字段被声明或volatile,java线程内存模型确保所有线程看到这个变量值是一致的。
讨论这些之前,我们先来简单的说一下java中的线程和java内存模型,在java内存模型中,分为了主内存和工作内存,所有的变量都存储在主内存中,每条线程还有自己的工作内存,工作内存中保存了被该线程使用到的变量的主内存副本拷贝,线程对变量所有的操作(读取,写入)都必须在工作线程中完成,而不能直接操作主内存中的变量,不同线程之间也无法直接访问对方工作内存中的变量,线程中变量值的传递必须通过主内存。大概用一下图来表示
这就是说 假如主内存中有一个变量值 int i = 0; 当多个线程去修改的时候,他们会把i从主内存拷贝到工作内存,修改完之后再写到主内存当中,我们知道在jvm中,为了提高性能,编译器和处理器会对指令做重排序,从java源码到最终实际执行的指令序列,分别经理下面三种重排序
举个简单的例子
int a = 10;
int b = 2;
int c = a+b;
在这里面 c依赖于ab 但ab之间不存在依赖关系,jvm只保证最后的计算结果是正确的,在执行的时候,有可能执行的是a+b 也有可能执行的是b+a 我们往往在开发过程中有个顺序执行的幻觉,程序是顺序执行的,这个正是因为as-if-serial语义把单线程程序保护了起来,让我们不用担心重排序的问题
那么在多线程的情况下呢?
public class ReorderExample {
int a = 0;
boolean flag = false;
public void writer(){
a = 1; //1
flag = true; //2
}
public void reader(){
if(flag){ //3
int i = a*a;//4
}
}
}
假如有两个线程A和B,A执行writer方法,随后B执行reader方法,线程B在操作4的时候,能否看到线程A操作1对共享变量a的写入呢?
答案是不一定能看到
原因是操作1和操作2没有数据依赖关系,这两个操作编译器可以对这两个操作进行重排序,同样 3和4也没有数据依赖关系,编译器和处理器也可以对这两个操作进行重排序,他们执行的顺序可能是 2 —> 3 —> 4 —> 1 这么一来,就发生顺序不一致的问题了,但这跟我们主题volatile有什么关系呢?
当声明共享变量为volatile后,这个变量的读写就会比较特别了
instance = new Singleton(); //instance是volatile变量
通过JIT编译器生成汇编指令来看 如下
0x01a3de1d: movb $0×0,0×1104800(%esi);
0x01a3de24: lock addl $0×0,(%esp);
有volatile修饰的变量,赋值之后会执行 lock addl $0×0,(%esp);的操作,这个操作相当于一个内存屏障,指令重排序的时不能把后面的指令重排序到内存屏障之前的位置;当只有一个CPU访问内存时,并不需要内存屏障,但如果有两个或者多个访问同一块内存的时候,且其中有一个在观测另一个,就需要内存屏障来保证一致性。
我们知道在C/C++上也有这个关键字,当两个线程都要用到某一个变量且该变量的值会被改变时,应该用volatile声明,该关键字的作用是防止优化编译器把变量从内存装入CPU寄存器中。如果变量被装入寄存器,那么两个线程有可能一个使用内存中的变量,一个使用寄存器中的变量,这会造成程序的错误执行。volatile的意思是让编译器每次操作该变量时一定要从内存中真正取出,而不是使用已经存在寄存器中的值