在读了《深入理解Java虚拟机》一书后,终于弄清楚了volatile的原理
主内存和工作内存
Java内存模型的主要目的是定义程序中各种变量的访问规则,即关注在虚拟机中把变量值存储到 内存和从内存中取出变量值这样的底层细节。此处的变量(Variables)与Java编程中所说的变量有所区 别,它包括了实例字段、静态字段和构成数组对象的元素,但是不包括局部变量与方法参数,因为后 者是线程私有的,不会被共享,自然就不会存在竞争问题。为了获得更好的执行效能,Java内存模 型并没有限制执行引擎使用处理器的特定寄存器或缓存来和主内存进行交互,也没有限制即时编译器 是否要进行调整代码执行顺序这类优化措施。
Java内存模型规定了所有的变量都存储在主内存(Main Memory)中(此处的主内存与介绍物理 硬件时提到的主内存名字一样,两者也可以类比,但物理上它仅是虚拟机内存的一部分)。每条线程 还有自己的工作内存(Working Memory,可与前面讲的处理器高速缓存类比),线程的工作内存中保 存了被该线程使用的变量的主内存副本,线程对变量的所有操作(读取、赋值等)都必须在工作内 存中进行,而不能直接读写主内存中的数据。不同的线程之间也无法直接访问对方工作内存中的变 量,线程间变量值的传递均需要通过主内存来完成,线程、主内存、工作内存三者的交互关系如图12- 2所示,注意与图12-1进行对比。
关于主内存与工作内存之间具体的交互协议,即一个变量如何从主内存拷贝到工作内存、如何从 工作内存同步回主内存这一类的实现细节,Java内存模型中定义了以下8种操作来完成。Java虚拟机实 现时必须保证下面提及的每一种操作都是原子的、不可再分的(对于double和long类型的变量来说, load、store、read和write操作在某些平台上允许有例外)。
lock(锁定):作用于主内存的变量,它把一个变量标识为一条线程独占的状态。
unlock(解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量 才可以被其他线程锁定。 ·read(读取):作用于主内存的变量,它把一个变量的值从主内存传输到线程的工作内存中,以 便随后的load动作使用。 ·load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的 变量副本中。
use(使用):作用于工作内存的变量,它把工作内存中一个变量的值传递给执行引擎,每当虚 拟机遇到一个需要使用变量的值的字节码指令时将会执行这个操作。
assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收的值赋给工作内存的变量, 每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
store(存储):作用于工作内存的变量,它把工作内存中一个变量的值传送到主内存中,以便随 后的write操作使用。
write(写入):作用于主内存的变量,它把store操作从工作内存中得到的变量的值放入主内存的 变量中。
关键字volatile可以说是Java虚拟机提供的最轻量级的同步机制
当一个变量被定义成volatile之后,它将具备两项特性:第一项是保证此变量对所有线程的可见 性,这里的“可见性”是指当一条线程修改了这个变量的值,新值对于其他线程来说是可以立即得知 的。而普通变量并不能做到这一点,普通变量的值在线程间传递时均需要通过主内存来完成。比如, 线程A修改一个普通变量的值,然后向主内存进行回写,另外一条线程B在线程A回写完成了之后再对 主内存进行读取操作,新变量值才会对线程B可见。
由于volatile变量只能保证可见性,在不符合以下两条规则的运算场景中,我们仍然要通过加锁 (使用synchronized、java.util.concurrent中的锁或原子类)来保证原子性:
运算结果并不依赖变量的当前值,或者能够确保只有单一的线程修改变量的值。
变量不需要与其他的状态变量共同参与不变约束。
使用volatile变量的第二个语义是禁止指令重排序优化,普通的变量仅会保证在该方法的执行过程 中所有依赖赋值结果的地方都能获取到正确的结果,而不能保证变量赋值操作的顺序与程序代码中的 执行顺序一致。因为在同一个线程的方法执行过程中无法感知到这点,这就是Java内存模型中描述的 所谓“线程内表现为串行的语义”。
在对volatile修饰的变量编译后的文件中发现
关键变化在于有volatile修饰的变量,赋值后(前面mov%eax,0x150(%esi)这句便 是赋值操作)多执行了一个“lock addl$0x0,(%esp)”操作,这个操作的作用相当于一个内存屏障 (Memory Barrier或Memory Fence,指重排序时不能把后面的指令重排序到内存屏障之前的位置,注 意不要与第3章中介绍的垃圾收集器用于捕获变量访问的内存屏障互相混淆),只有一个处理器访问内 存时,并不需要内存屏障;但如果有两个或更多处理器访问同一块内存,且其中有一个在观测另一 个,就需要内存屏障来保证一致性了。
这句指令中的“addl$0x0,(%esp)”(把ESP寄存器的值加0)显然是一个空操作,之所以用这个空 操作而不是空操作专用指令nop,是因为IA32手册规定lock前缀不允许配合nop指令使用。这里的关键 在于lock前缀,查询IA32手册可知,它的作用是将本处理器的缓存写入了内存,该写入动作也会引起 别的处理器或者别的内核无效化(Invalidate)其缓存,这种操作相当于对缓存中的变量做了一次前面 介绍Java内存模式中所说的“store和write”操作。所以通过这样一个空操作,可让前面volatile变量的 修改对其他处理器立即可见。
本节的最后,我们再回头来看看Java内存模型中对volatile变量定义的特殊规则的定义。假定T表示 一个线程,V和W分别表示两个volatile型变量,那么在进行read、load、use、assign、store和write操作 时需要满足如下规则:
只有当线程T对变量V执行的前一个动作是load的时候,线程T才能对变量V执行use动作;并且, 只有当线程T对变量V执行的后一个动作是use的时候,线程T才能对变量V执行load动作。线程T对变量 V的use动作可以认为是和线程T对变量V的load、read动作相关联的,必须连续且一起出现。
(即使用前必须从主存去拿最新的数据)
只有当线程T对变量V执行的前一个动作是assign的时候,线程T才能对变量V执行store动作;并 且,只有当线程T对变量V执行的后一个动作是store的时候,线程T才能对变量V执行assign动作。线程 T对变量V的assign动作可以认为是和线程T对变量V的store、write动作相关联的,必须连续且一起出现。
(即每次修改完数据后必须立刻同步回主存,注意,上面说到的lock语句,会在写回主存的时候让其他使用了变量的线程重新去获取最新的值,即刚刚修改同步回主存的值,达到了通知的效果)
由此可见,volatile也并不能真正的保证原子性,真正的为了安全还是要使用synchronized关键字。但volatile可以在一些配置信息中使用到,其他信息都依赖于某几个变量的状态可以考虑使用volatile,如:
volatile boolean shutdownRequested; ------ public void shutdown() { shutdownRequested = true; } ------ public void doWork() { while (!shutdownRequested) { // 代码的业务逻辑 } }