Volatile关键字

背景

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()方法返回之后的操作。