java中的volatile有两个语义:
- 保证共享变量可见性
通俗来说就是,某个线程对一个volatile变量的修改,对于其它线程来说是可见的,即线程每次获取volatile变量的值都是最新的。
- 保证共享变量可见性
- 禁止指令重排序
1.java内存模型
- 线程之间的共享变量存储在主内存中,所谓的共享变量,是指所有存储在堆内存上的实例域、静态域和数组元素存储在堆内存中。
- 每个线程都有一个私有的本地内存(local memory),本地内存中存储了该线程读/写共享变量的副本。本地内存是JMM的一个抽象概念,并不真实存在。它涵盖了缓存,写缓冲区,寄存器以及其他的硬件和编译器优化。
2.重排序
在执行程序时为了提高性能,编译器和处理器常常会对指令做重排序。重排序有以下三种类型:
- 编译器优化的重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。下面代码中a、b的赋值顺序,被编译之后可能就变成了先设置b,再设置a。
public void set() {
a = 1;
b = 1;
}
- 指令级并行的重排序。现代处理器采用了指令级并行技术(Instruction-Level Parallelism, ILP)来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。
-
内存系统的重排序。由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。
1、CPU执行load读数据时,把读请求放到LoadBuffer,这样就不用等待其它CPU响应,先进行下面操作,稍后再处理这个读请求的结果。
2、CPU执行store写数据时,把数据写到StoreBuffer中,待到某个适合的时间点,把StoreBuffer的数据刷到主存中。
由于StoreBuffer和LoadBuffer是异步执行的,所以在外面看来,先写后读,还是先读后写,没有严格的固定顺序。
从java源代码到最终实际执行的指令序列,会分别经历下面三种重排序:
上述的1属于编译器重排序,2和3属于处理器重排序。这些重排序都可能会导致多线程程序出现内存可见性问题。
下面举例说明处理器重排序带来的可见性问题
假设处理器A和处理器B按程序的顺序并行执行内存访问,最终却可能得到x = y = 0的结果。具体的原因如下图所示:
这里处理器A和处理器B可以同时把共享变量写入自己的写缓冲区(A1,B1),然后从内存中读取另一个共享变量(A2,B2),最后才把自己写缓存区中保存的脏数据刷新到内存中(A3,B3)。当以这种时序执行时,程序就可以得到x = y = 0的结果。
从内存操作实际发生的顺序来看,直到处理器A执行A3来刷新自己的写缓存区,写操作A1才算真正执行了。虽然处理器A执行内存操作的顺序为:A1->A2,但内存操作实际发生的顺序却是:A2->A1。此时,处理器A的内存操作顺序被重排序了(处理器B的情况和处理器A一样,这里就不赘述了)。
这里的关键是,由于写缓冲区仅对自己的处理器可见,它会导致处理器执行内存操作的顺序可能会与内存实际的操作执行顺序不一致。由于现代的处理器都会使用写缓冲区,因此现代的处理器都会允许对写-读操做重排序。
那么JMM是如何禁止重排序从而保证可见性的呢?
- 对于编译器,JMM的编译器重排序规则会禁止特定类型的编译器重排序(不是所有的编译器重排序都要禁止)。
- 对于处理器重排序,JMM的处理器重排序规则会要求java编译器在生成指令序列时,插入特定类型的内存屏障(memory barriers,intel称之为memory fence)指令,通过内存屏障指令来禁止特定类型的处理器重排序(不是所有的处理器重排序都要禁止)。
3.内存屏障
跟据上面的描述,为了保证内存可见性,java编译器在生成指令序列的适当位置会插入内存屏障指令来禁止特定类型的处理器重排序。内存屏障是一组处理指令,用来实现对内存操作的顺序限制。JMM把内存屏障指令分为下列四类:
4. happens-before
从JDK5开始,java使用新的JSR -133内存模型(本文除非特别说明,针对的都是JSR- 133内存模型)。JSR-133提出了happens-before的概念,通过这个概念来阐述操作之间的内存可见性。如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须存在happens-before关系。这里提到的两个操作既可以是在一个线程之内,也可以是在不同线程之间。与程序员密切相关的happens-before规则如下:
- 程序顺序规则:一个线程中的每个操作,happens- before 于该线程中的任意后续操作。
- 监视器锁规则:对一个监视器锁的解锁,happens- before 于随后对这个监视器锁的加锁。
- volatile变量规则:对一个volatile域的写,happens- before 于任意后续对这个volatile域的读。
- 传递性:如果A happens- before B,且B happens- before C,那么A happens- before C。
happens-before与JMM的关系如下图所示:
如上图所示,一个happens-before规则通常对应于多个编译器重排序规则和处理器重排序规则。对于java程序员来说,happens-before规则简单易懂,它避免程序员为了理解JMM提供的内存可见性保证而去学习复杂的重排序规则以及这些规则的具体实现。
5.volatile的禁止重排序与保证可见性
- 编译器对操作volatile变量的代码不再进行优化
- volatile底层通过内存屏障实现禁止特定类型的处理器重排序
为了实施volatile对应的happens-before规则,对volatile变量的内存操作涉及到的内存屏障如下:
以volatile store -> volatile load为例,通过插入StroreLoad内存屏障,确保Store数据对其他处理器变得可见(指刷新到内存),之前于Load2及所有后续装载指令的装载。从而实现,某个线程对volatile变量的修改会立即刷新到主内存,并导致其它线程工作内存中的副本无效,读取时只能从主内存加载最新的值。
参考链接:
1.http://www.infoq.com/cn/articles/java-memory-model-1
2.http://www.importnew.com/23520.html