每日一问:谈谈 volatile 关键字

这是 wanAndroid 每日一问中的一道题,下面咱们来尝试解答一下。java

讲讲并发专题 volatile,synchronize,CAS,happens before, lost wake upandroid

为了本系列的「短平快」,今天咱们就来第一个主角:volatile安全

保证内存可见性

前面咱们讲到:Java 内存模型分为了主内存和工做内存两部分,其规定程序全部的变量都存储在主内存中,每条线程还有本身的工做内存,线程的工做内存中保存了该线程使用到的变量的主内存副本拷贝,线程对变量的全部操做(赋值、读取等)都必须在工做内存中进行,而不能直接读取主内存中的变量。不一样线程之间也没法直接访问对方工做内存中的变量,线程间变量值的传递都必须通过主内存的传递来完成。
多线程

这样就会存在一个状况,工做内存值改变后到主内存更新必定是须要必定时间的,因此可能会出现多个线程操做同一个变量的时候出现取到的值仍是未更新前的值。并发

这样的状况咱们一般称之为「可见性」,而咱们加上 volatile 关键字修饰的变量就能够保证对全部线程的可见性。app

这里的可见性是什么意思呢?当一个线程修改了变量的值,新的值会马上同步到主内存当中。而其余线程读取这个变量的时候,也会从主内存中拉取最新的变量值。优化

为何 volatile 关键字能够有这样的特性?这得益于 Java 语言的先行发生原则(happens-before)。简单地说,就是先执行的事件就应该先获得结果。线程

可是! volatile 并不能保证并发下的安全。code

Java 里面的运算并不是原子操做,好比 i++ 这样的代码,实际上,它包含了 3 个独立的操做:读取 i 的值,将值加 1,而后将计算结果返回给 i。这是一个「读取-修改-写入」的操做序列,而且其结果状态依赖于以前的状态,因此在多线程环境下存在问题。blog

要解决自增操做在多线程下线程不安全的问题,能够选择使用 Java 提供的原子类,如 AtomicInteger 或者使用 synchronized 同步方法。

原子性:在 Java 中,对基本数据类型的变量的读取和赋值操做是原子性操做,即这些操做是不可被中断的,要么执行,要么不执行。也就是说,只有简单的读取、赋值(并且必须是将数字赋值给某个变量)才是原子操做。(变量之间的相互赋值不是原子操做,好比 y = x,其实是先读取 x 的值,再把读取到的值赋值给 y 写入工做内存)

禁止指令重排

最开始看到「指令重排」这个词语的时候,我也是一脸懵逼。后面看了相关书籍才知道,处理器为了提升程序效率,可能对输入代码进行优化,它不保证各个语句的执行顺序同代码中的顺序一致,可是它会保证程序最终执行结果和代码顺序执行的结果是一致的。

指令重排是一把双刃剑,虽然优化了程序的执行效率,可是在某些状况下,却会影响到多线程的执行结果。好比下面的代码:

boolean contextReady = false;
//在线程A中执行:
context = loadContext();    // 步骤 1
contextReady = true;        // 步骤 2

//在线程B中执行:
while(!contextReady ){ 
   sleep(200);
}
doAfterContextReady (context);

以上程序看似没有问题。线程 B 循环等待上下文 context 的加载,一旦 context 加载完成,contextReady == true 的时候,才执行 doAfterContextReady 方法。
可是,若是线程 A 执行的代码发生了指令重排,也就是上面的步骤 1 和步骤 2 调换了顺序,那线程 B 就会直接跳出循环,直接执行 doAfterContextReady() 方法致使出错。

volatile 采用「内存屏障」这样的 CPU 指令就解决这个问题,不让它指令重排。

使用场景

从上面的总结来看,咱们很是容易得出 volatile 的使用场景:

  1. 运行结果并不依赖变量的当前值,或者可以确保只有单一的线程修改变量的值。
  2. 变量不须要与其余的状态变量共同参与不变约束。

好比下面的场景,就很适合使用 volatile 来控制并发,当 shutdown() 方法调用的时候,就能保证全部线程中执行的 work() 当即停下来。

volatile boolean shutdownRequest;
private void shutdown(){
    shutdownRequest = true;
}
private void work(){
    while (!shutdownRequest){
        // do something
    }
}

总结

说了这么多,其实对于 volatile 咱们只须要知道,它主要特性:保证可见性、禁止指令重排、解决 long 和 double 的 8 字节赋值问题。

还有一个比较重要的是:它并不能保证并发安全,不要和 synchronize 混淆。

细心的你还会发现,在 Kotlin 语言中,实际上是没有 volatilesynchronize 这样的关键字的,那 Kotlin 是怎么处理并发问题的呢?感兴趣的必定要去看看。

文章参考:
漫画:什么是volatile关键字?(整合版) 《深刻理解 Java 虚拟机》

相关文章
相关标签/搜索