深入理解Volatile关键字及其实现原理

volatile通常被比喻成”轻量级的synchronized”,是Java中提供的另一种解决可见性和有序性问题的方案。

volatile的原理

可见性实现

为了提高处理器的执行速度,在处理器和内存之间增加了多级缓存来提升。但是由于引入了多级缓存,就存在缓存数据不一致问题。在多处理器下,为了保证各个处理器的缓存是一致的,就会实现缓存一致性协议。

缓存一致性协议:每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器要对这个数据进行修改操作的时候,会强制重新从系统内存里把数据读到处理器缓存里。

对于volatile变量,当对volatile变量进行写操作的时候,JVM会向处理器发送一条lock前缀的指令,将这个缓存中的变量回写到系统主存中。而其他处理器的缓存由于遵守了缓存一致性协议,也会把这个变量的值从主存加载到自己的缓存中。这就保证了一个volatile在并发编程中,其值在多个缓存中是可见的。

每个线程的工作内存是CPU寄存器和高速缓存的抽象。

两条线程Thread-A与Threab-B同时操作主存中的一个volatile变量i时,Thread-A写了变量i,那么:

Thread-A发出LOCK#指令 发出的LOCK#指令锁总线(或锁缓存行),同时让Thread-B高速缓存中的缓存行内容失效。

Thread-A向主存回写最新修改的i Thread-B读取变量i,那么:

Thread-B发现对应地址的缓存行被锁了,等待锁的释放,缓存一致性协议会保证它读取到最新的值

由此可以看出,volatile关键字的读和普通变量的读取相比基本没差别,差别主要还是在变量的写操作上。

有序性

Happen-before

JSR 133中对Happen-before的定义:如果a happen-before b,则a所做的任何操作对b是可见的。

happen-before规则:

同一个线程中的,前面的操作 happen-before 后续的操作。(即单线程内按代码顺序执行。但是,在不影响在单线程环境执行结果的前提下,编译器和处理器可以进行重排序,这是合法的。换句话说,这一是规则无法保证编译重排和指令重排)。
监视器上的解锁操作 happen-before 其后续的加锁操作。(Synchronized 规则)
对volatile变量的写操作 happen-before 后续的读操作。(volatile 规则)
线程的start() 方法 happen-before 该线程所有的后续操作。(线程启动规则)
线程所有的操作 happen-before 其他线程在该线程上调用 join 返回成功后的操作。
如果 a happen-before b,b happen-before c,则a happen-before c(传递性)。

有序性即程序执行的顺序按照代码的先后顺序执行。

除了引入了时间片以外,由于处理器优化和指令重排等,CPU还可能对输入代码进行乱序执行,这就是可能存在有序性问题。

而volatile除了可以保证数据的可见性之外,还有一个强大的功能,那就是他可以禁止指令重排优化等。

普通的变量仅仅会保证在该方法的执行过程中所依赖的赋值结果的地方都能获得正确的结果,而不能保证变量的赋值操作的顺序与程序代码中的执行顺序一致。

volatile可以禁止指令重排,这就保证了代码的程序会严格按照代码的先后顺序执行。这就保证了有序性。被volatile修饰的变量的操作,会严格按照代码顺序执行。

volatile与synchronized的区别

  • (1)使用上的区别:Volatile只能修饰变量,synchronized只能修饰方法和语句块
  • (2)对原子性的保证:synchronized可以保证原子性,Volatile不能保证原子性
  • (3)对可见性的保证:都可以保证可见性,但实现原理不同,Volatile对变量加了lock,synchronized使用monitorEnter和monitorexit monitor JVM
  • (4)对有序性的保证:Volatile能保证有序,synchronized可以保证有序性,但是代价(重量级)并发退化到串行
  • (5)synchronized引起阻塞,Volatile不会引起阻塞