Java并发编程之基础篇--volatile关键字

前言

在Java并发编程中,有一个关键字是volatile,它的英文意思是“易变的,不稳定的,无定型的”。那么在Java编程中,被volatile修饰的变量,它能够保证当前变量的可见性,从而使所有访问该前变量的线程都能够及时的获取到当前变量的最新值,从而保证它的可见性。那么它是怎么做到的呢?

volatile的内存语义

被volatile修饰的共享变量进行写操作时,CPU会对当前写操作添加一个LOCK前缀的汇编指令,而这个指令会在多核CPU下发生如下两件事情:

  1. 将当前处理器缓存行的数据写回到系统内存中
  2. 这个写回到内存的操作,会使其他CPU缓存中的缓存了该内存地址的数据无效

CPU为了提高处理速度,不会直接读取内存的内容,而是将内存的数据存放到CPU缓存(一般分为L1/L2/L3三级缓存)中然后再进行操作,多核的CPU中每个核都有自己的缓存,但是这样不能确保什么时候将CPU缓存的内容写到缓存中去
如果对变量添加了volatile的修饰后,在对变量进行写操作的时候,JVM就会向CPU发送一条LOCK前缀的指令,将这个变量所在的缓存行的数据写回到内存中去。如果此时有其他核的CPU也对该变量进行了写操作,那么当前核的CPU会根据缓存数据一致性协议来处理数据的一致性(每个处理器通过嗅探在总线上传播的数据来检查自己的缓存数据是否过期,如果发现自己缓存行对应的内存地址也就是内存值被修改,就会将当前的缓存设置为无效状态,当处理器进行写操作的时候,重新读取内存的值到缓存中去)。

两条原则的说明

  1. LOCK指令会使处理器将CPU缓存中修改的内容写入到内存中

LOCK前缀的指令会导致在执行指令期间,声言处理器的LOCK#信号。在多处理器的环境中,LOCK#信号确保在声言该信号期间,处理器可以独占任何共享内存。但是,在当前主流的处理器里,LOCK#信号一般不锁总线,而是锁缓存,毕竟锁总线开销的比较大(之前的处理器总是在总线上声言LOCK#信号)。当前主流的处理器里,如果访问的内存区域已经缓存到了处理器内部,则不会声言LOCK#信号,而是锁定当前的内存区域的缓存并写回到内存中,并通过缓存一致性协议来确保修改的原子性,此操作被称作“缓存锁定”,缓存一致性协议来保证修改内存数据时不允许两个以上处理器进行修改。

  1. 一个处理器的缓存回写到内存会导致其他处理器的缓存无效

IA-32处理器和Intel 64处 理器使用MESI(修改、独占、共享、无效)控制协议去维护内部缓存和其他处理器缓存的一致性。在多核处理器系统中进行操作的时候,IA-32和Intel 64处理器能嗅探其他处理器访问系统 内存和它们的内部缓存。处理器使用嗅探技术保证它的内部缓存、系统内存和其他处理器的缓存的数据在总线上保持一致。例如,在Pentium和P6 family处理器中,如果通过嗅探一个处理 器来检测其他处理器打算写内存地址,而这个地址当前处于共享状态,那么正在嗅探的处理 器将使它的缓存行无效,在下次访问相同内存地址时,强制执行缓存行填充。

volatile在java内存模型(JMM)的语义

被volatile修饰的变量,在JMM内存模型中也有明确的语义规定(具体的可以参考JSR-133中的内存模型语义)中遵循happends-before原则,即:对一个volatile域的写,happens-before于任意后续对这个volatile域的读。换句话来说就是,对于一个volatile的读,它总是能看到任意线程对这个变量的最后写入

volatile在JMM中的语义的实现

它的实现,我们要清楚一个概念--指令重排序。编译器会对被volatile修饰的变量的指令重排进行规定:

  1. 当第二个操作是volatile写时,不管第一个操作是什么(普通的读写代码还是被volatile修饰的变量的相关读写代码),都不能重排序。这个规则确保 volatile写之前的操作不会被编译器重排序到volatile写之后。
  2. 当第一个操作是volatile读时,不管第二个操作是什么,都不能重排序。这个规则确保 volatile读之后的操作不会被编译器重排序到volatile读之前。
  3. 当第一个操作是volatile写,第二个操作是volatile读时,不能重排序。
    为了实现volatile的内存语义,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。

重排序是指编译器和处理器为了优化程序性能而对指令序列进行重新排序的一种手段。
内存屏障是指是一组处理器指令,用于实现对内存操作的顺序限制。

总结

volatile关键字修饰的变量,使JIT编译器在编译时,向CPU发送的汇编指令时添加了LOCK前缀的指令,从而使CPU在读取了内存的共享变量后,如果进行修改操作,会立即将CPU缓存中的数据写入到内存中去,并且通过MESI控制协议来保证写入内存操作的原子性。
注意:但是volatile关键字只是保证了变量在内存中修改的可见性,对任意单个的volatile变量的读写具有原子性,但类似于i++的复合操作不具有原子性

参考文献

《Java并发编程的艺术》

原文地址:https://www.cnblogs.com/mr-ziyoung/p/13578155.html