Java中的volatile关键字

 概要

若是不存在并发同步状况时,编译器或运行时或处理器会应用各类优化。而缓存和重排序则是并发上下文中的优化手段,Java和JVM提供了许多控制内存顺序的方法,volatile关键字就是其中之一。程序员

没有volatile会怎么样?

看下面的一个例子:面试

public class TaskRunner {

    private static int number;
    private static boolean ready;

    private static class Reader extends Thread {

        @Override
        public void run() {
            while (!ready) {
                Thread.yield();
            }

            System.out.println(number);
        }
    }

    public static void main(String[] args) {
        new Reader().start();
        number = 42;
        ready = true;
    }
}

TaskRunner类维护两个简单的变量。在它的main方法中,它建立了另外一个线程,只要它是false,它就会在ready变量上自旋。当变量变为true时,线程将打印number变量。缓存

咱们指望这个程序在短暂的延迟后简单地打印42。然而,实际上这个延迟可能会更长。它甚至可能永远挂起,甚至打印0。多线程

这些异常的缘由是缺少适当的内存可见性和重排序,贴合本文来讲,就是没有使用volatile关键字修饰变量。并发

内存可见性

简单来讲,多线程运行在多个CPU上,而每一个线程都会有本身的的cache,所以没法保证从主存中读取数据的顺序,即没法保证各个CPU上的线程读取的变量数据一致。ide

结合上面的程序,主线程在其核心缓存中保留了ready和number的副本,而Reader线程也是一样保留了副本,以后主线程更新缓存值。在大多数现代处理器上,写入请求在发出后不会当即应用。事实上,处理器倾向于将这些写入排在一个特殊的写入缓冲区中。一段时间后,它们会一次性将这些写入应用到主内存中。性能

所以当主线程更新number和ready变量时,没法保Reader线程会看到什么。换句话说,Reader线程可能会当即看到更新的值,或者有一些延迟,或者根本不会。优化

重排序

上面提到过,除了一直死循环外,程序还有小几率打印出0,这就是重排序的缘由。在CPU执行指令时,先更新了ready变量而后执行的线程操做。spa

从新排序是一种用于提升性能的优化技术,不一样的组件可能会应用这种优化:线程

  • 处理器能够按程序顺序之外的任何顺序刷新其写缓冲区
  • 处理器可能会应用乱序执行技术
  • JIT编译器能够经过从新排序进行优化
volatile关键字

那么volatile关键字干了什么呢?

volatile关键字在汇编阶段对变量加上Lock前缀指令,经过MESI缓存一致性协议来保证线程之间的可见性,任意线程对变量的修改都会被同一时间同步到全部读取该变量的线程CPU上,简单来讲,一个改了就能保证全部的都改了。

这里先看汇编层的Lock指令,早期CPU采起锁总线的方式来实现这个指令,仲裁器选择一个CPU独占总线,从而使其余CPU没法经过总线与内存通信,实现原子性;固然这种方式效率低,如今通常采用cache locking,这种场景下的数据一致是经过MESI缓存一致性协议来完成的。

这里再也不详细说明缓存一致性协议,主要思想是CPU会不断嗅探总线上的数据交换,当一个缓存表明它所在的CPU去读写内存时,其余CPU都会获得通知,从而同步本身的缓存。

在Java内存模型中,存在着原子操做,这些原子操做与Java内存模型控制并发有着关键做用。

  • read(读取):从主内存读取数据
  • load(载入):将主内存读取到的数据写入工做内存,即缓存
  • use(使用):从工做内存读取数据来计算
  • assign(赋值):将计算好的值从新赋值到工做内存中
  • store(存储):将工做内存数据写入主内存
  • write(写入):将store过去的变量值赋值给主内存中的变量
  • lock(锁定):将主内存变量加锁,标识为线程独占状态
  • unlock(解锁):将主内存变量解锁,解锁后其余线程能够锁定该变量

在volatile关键字修饰下,store和write操做必须是连续的,组合成了原子操做,修改后必须当即同步到主内存,使用时必须从主内存刷新,由此保证volatile可见性。

同时,volatile关键字也采用内存屏障来禁止指令重排。volatile变量的内存可见性影响超出了volatile变量自己。

更具体地说,假设线程A写入一个volatile变量,而后线程B读取同一个volatile变量。在这种状况下,在写入volatile变量以前对A可见的值将在读取volatile变量后对B可见:

a022d94d42d3f9a531c10360ec46ee69.png

从技术上讲,对volatile字段的任何写入都发生在同一字段的每次后续读取以前。这是Java 内存模型的volatile变量规则。

因为内存排序的长处,有时咱们能够捎带volatile的可见性属性另外一个变量。例如,在咱们的示例中,咱们只须要将ready变量标记为volatile:

public class TaskRunner {

    private static int number; // not volatile
    private volatile static boolean ready;

    // same as before
}

在读取ready变量以后,任何在将ready变量写为true以前的内容对任何内容都是可见的。所以,number变量会捎带上ready变量强制执行的内存可见性。简而言之,即便它不是volatile变量,它也表现出volatile行为。

经过利用这些语义,咱们能够将类中的少数变量定义为volatile并优化可见性。

最后

最近我整理了整套《JAVA核心知识点总结》,说实话 ,做为一名Java程序员,不论你需不须要面试都应该好好看下这份资料。拿到手老是不亏的~个人很多粉丝也所以拿到腾讯字节快手等公司的Offer

Java进阶之路群,找管理员获取哦-!

35c27fe92ab418c3158824ecb093111b.png

99d086cf20ee5916b4c79ed22bc55a6c.png

相关文章
相关标签/搜索