volatile关键字

starlin 681 2018-05-29

在java语言规范中对volatile的定义如下:

Java编程语言允许线程访问共享变量,为了确保共享变量能被准确和一致地更新,线程应该确保通过排他锁单独获得这个变量。

简单点来说就是一个变量如果用volatile修饰了,则java可以确保所有线程看到这个变量的值是一致,如果某个线程对volatile修饰的共享变量进行更新,那么其他线程可以立马看到这个更新,这就是所谓的线程可见性。

volatile关键字的三个特征是:线程可见、不具备原子性、禁止指令重排,volatile 的读性能消耗与普通变量几乎相同,但是写操作稍慢,因为它需要在本地代码中插入许多内存屏障指令来保证处理器不发生乱序执行

内存模型相关概念

可见性

可见性是指当多个线程同时访问一个变量时,一个线程修改了该变量的值,其他线程能够立即看到修改后的值

在多线程环境下,一个线程对共享变量的操作对其他线程是不可见的

java提供了volatile来保证可见性

当一个变量被volatile修饰后,表示着线程本地内存无效,当一个线程修改共享变量后会立即更新至主内存,当其他线程读取共享变量时,会直接从主内存中读取。

原子性

原子性:即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。

例如i++这个操作,在单线程环境下,可以认为是原子操作,但是在多线程环境下,Java只保证了基本数据类型的变量和赋值操作才是原子性的,在多线程环境下,需通过synchronized、锁来保证。

volatile是无法保证复合操作的原子性

有序性

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

在Java内存模型中,为了效率是允许编译器和处理器对指令进行重排序,当然重排序它不会影响单线程的运行结果,但是对多线程会有影响。

volatile原理

volatile可以保证线程可见性且提供了一定的有序性,但是无法保证原子性,在JVM层volatile采用的是"内存屏障"来实现的。

简单说就是:
1. volatile保证可见性,但不保证原子性
2. 禁止指令重排序

在执行程序是为了提高效率,是允许编译器和处理器对指令进行重排序

  1. 编译器重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序
  2. 处理器重排序。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序

指令重排序对单线程没有什么影响,他不会影响程序的运行结果,但是会影响多线程的正确性。既然指令重排序会影响到多线程执行的正确性,那么我们就需要禁止重排序。

那volatile怎么样来实现禁止指令重排序了?

下面这段话摘自《深入理解Java虚拟机》:
“观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令”
lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:

  1. 它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;

  2. 它会强制将对缓存的修改操作立即写入主存;

  3. 如果是写操作,它会导致其他CPU中对应的缓存行无效。

总结

总体来说,volatile是并发编程中的一种优化,在某些场景下可以代替Synchronized。但是,volatile的不能完全取代Synchronized的位置,只有在一些特殊的场景下,才能适用volatile。总的来说,必须同时满足下面两个条件才能保证在并发环境的线程安全:

  1. 对变量的写操作不依赖于当前值。
  2. 该变量没有包含在具有其他变量的不变式中。

参考

深入分析volatile的实现原理


# java并发