背景
volatile关键字是并发编程中的一个比较重要的关键字。它能保证变量/对象在内存中的可见性,同时禁止指令重排序,避免了CPU或者编译器优化带来的可见性问题。
在并发编程中,volatile可以去修饰一个变量,或者是一个对象(比如单例模式中就使用了volatile去修饰单例对象)
举例说明
volatile int a = 100;
volatile SingleInstance instance;*
什么是可见性?什么是可见性问题?
答: 可见性指的是一个共享变量被线程修改了以后,其他线程能立即看到变更后的变量值。因为线程是在各自的工作内存中执行数据的逻辑操作,并不会操作到主内存的变量值,一旦线程更新了变量的值,如果想要被可见,就必须立即再更新至主内存。所以,可见性问题就是指,A线程更新了变量的值,其他B线程操作变量的时候,没有得到这个变量变更后的新值,也就是A线程修改的值不可见。B线程使用旧值对变量进行更新操作,从而使得数据不一致。这个就是可见性问题!
如何解决变量可见性问题?
答:java中解决可见性问题的方案有很多。比如synchronzied, volatile, Lock锁,Atomic包下的原子类,JUC下的类。
synchronzied主要是保证同一时刻只能有一个线程操作某一个共享变量。避免多个线程同时访问一个共享变量带来的可见性问题。
volatile主要是会确保每个线程都能从主内存中读取该变量的最新值,而不是从自己的缓存中读取。本篇文章主要讲volatile的底层原理。
volatile的实现原理
volatile是通过内存屏障来禁止指令重排序,从而保证可见性的。
内存屏障有写屏障和读屏障(这些屏障实际上是一些硬件或者编译器级别的指令), volatile变量更新以后,会立即调用store指令,确保之前所有的写操作都会刷新到主内存中,避免写操作的重排序。读屏障确保读取volatile变量之前,会从主内存中读取最新的值。大多数处理器用的都是StoreLoad屏障。
StoreLoad相当于是一个全屏障,它会把处理器给变量赋值的指令存储到Store Buffer, 然后lock指令使到Store Buffer中的数据刷新到缓存行,同时使得其他CPU缓存了变量的缓存行失效。
所以说内存屏障底层其实还是调用了Lock指令。
happens before模型
这个JMM中的一些规范,主要是描述了两个操作指令的顺序关系。如果A操作和B操作存在happens-before的关系,那么意味着A操作的执行结果对B操作可见。
以下是一些Happens-before规则
Happens-before规则
-
程序顺序原则
在同一个线程中,如果x操作在Y操作之前,那么x happens before y,其实也是as-if-serial语义。 -
传递性规则
如果存在A happens before B; B happens before C, 那么必然会存在A happens before C 。 -
volatile变量规则
指的是通过内存屏障来保障一个volatile修饰的变量的写操作一定happens before于其读操作。 -
监视器锁规则
一个线程对一个锁的释放操作一定happens before后续线程对该锁的加锁操作。public void monitor() {
synchronzied(this) {
if(x == 0) {
x = 10;
}
}
如代码所示,当A线程执行逻辑之前,加上锁,执行结束后,会释放锁,此时A产生的结果对B一定是可见的。
- start规则
假如一个线程A调用子线程的start方法,那么线程A在调用start()方法之前的所有操作都happens-before线程B中的所有操作。 - join规则
首先join()的作用是等待某个子线程的执行结果。
如果主线程main()执行了线程A的join()方法并且成功返回,那么线程A中的任意操作happens before 于main线程的join()方法返回之后的操作。