前言
在讲volatile之前先看一个例子
Processor A | Processor B | |
---|---|---|
代码 | a=1;//A1 x=b;//A2 | b=2;//B1 y=a;//B2 |
假设处理器A和处理器B按顺序并行执行内存访问,最终可能得到x=y=0的结果。
首先,大家可能都知道对于线程A与线程B而言,各自的处理器都只能看到自己缓冲区里面的数据,要想看到其他线程的数据,必须在主内存里面获取。这就有可能线程A要读取变量b的值的时候,线程B还没有将b的值放进主内存。这就导致了线程A读到的变量b是0。
由于写缓冲区仅对自己的处理器可见,它会导致处理器执行内存操作的顺序可能与实际的操作执行顺序不一样。由于现代的处理器都会使用写缓冲区,因此现代的处理器都会允许写-读操作进行重排序。
happens-before
在JMM中,如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须要存在happens-before关系。这里提到的两个操作既可以是一个线程内的,也可以是不同线程内的。
happens-before的规则如下:
程序顺序规则:一个线程中的每个操作,happens-before于该线程中的任意后续操作。
监视器锁规则:对一个锁的解锁,happens-before于随后对这个锁的加锁。
volatile变量规则:对一个volatile域的写,happens-before于惹你后续对这个域的读。
传递性:如果A happens-before B,且B happens-before C,那么Ahappens-beforeC。
这里需要注意的是,A happens-before B不代表A一定要在B之前执行,JMM仅仅要求钱一个操作(执行的结果)对后一个操作可见,且前一个操作按顺序排在第二个操作之前。
重排序
重排序是指编译器和处理器为了优化程序性能而对指令序列进行重新排序的一种手段。
数据依赖性
如果两个操作访问同一个变量,且这两个操作中有一个是写操作,此时这两个操作之间就存在数据依赖性。这里所说的数据依赖性仅针对单个处理器中执行的指令系列和单个线程中执行的操作。不同处理器之间和不同线程之间的数据依赖性不被编译器和处理器锁考虑。
as-if-serial
as-if-serial的意思是:不管怎么重排序,程序的执行结果不能被改变。编译器,runtime和处理器都必须遵守as-if-serial语义。
为了遵守as-if-serial语义,编译器和处理器都不会对存在数据以来关系的操作做重排序,因为这种重排序是改变执行结果。但是,如果操作之间不存在数据以来关系,这些操作就可能被编译器和处理器重排序。
重排序对多线程的影响
看下面的代码`
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
}
}
}
flag变量是个标记,标记变量a是否已经被写入。假设有线程A和B,A首先执行writer()方法,随后B线程接着执行reader()方法,线程B在执行操作4的时候,是不一定能够看到线程A在操作1对共享数据a的写入的。因为操作1和操作2没有数据依赖关系,编译器可以对他们进行重排序,操作3和操作4也是同理。所以它们的执行顺序有可能是2-3-4-1,所以线程B在执行操作4的时候,不一定能够看到线程A在操作1对共享数据a的写入的。而因为操作3和操作4存在数据以来关系,如果他们重排序了会对结果产生影响,所以编译器和处理器禁止对操作3和操作4进行重排序。
总结
以上所讲的是这几天的学习内容,是未同步内存模型,这种未同步的内存模型的运行顺序具有很大的随机性,可能每次运行的结果都不一样,无法预知。线程执行时读取到的值,要么是之前某个线程写入的值,要么是默认值(0,null,false),JMM保证线程读操作读取到的值不会是无中生有的。为了实现最小安全性,JVM在堆上分批对象时,首先会对内存空间进行清理,然后才会在上面分配对象。
下一篇博客会开始写顺序一致性模型,也就是可以预料结果的模型。