zoukankan      html  css  js  c++  java
  • 面试官最爱的 volatile 关键字,这些问题你都搞懂了没?

    前言

    volatile相关的知识点,在面试过程中,属于基础问题,是必须要掌握的知识点,如果回答不上来会严重扣分的哦。

    volatile关键字基本介绍

    volatile可以看成是synchronized的一种轻量级的实现,但volatile并不能完全代替synchronized,volatile有synchronized可见性的特性,但没有synchronized原子性的特性。

    可见性即用volatile关键字修饰的成员变量表明该变量不存在工作线程的副本,线程每次直接都从主内存中读取,每次读取的都是最新的值,这也就保证了变量对其他线程的可见性。

    另外,使用volatile还能确保变量不能被重排序,保证了有序性。

    • 当一个变量定义为volatile之后,它将具备两种特性:

      • 保证此变量对所有线程的可见性
      • 禁止指令重排序优化
    • volatile与synchronized的区别:

      • 1、volatile只能修饰实例变量和类变量,而synchronized可以修饰方法,以及代码块。
      • 2、volatile保证数据的可见性,但是不保证原子性; 而synchronized是一种排他(互斥)的机制,既保证可见性,又保证原子性。
      • 3、volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞。
      • 4、volatile可以看做是轻量版的synchronized,volatile不保证原子性,但是如果是对一个共享变量进行多个线程的赋值,而没有其他的操作,那么就可以用volatile来代替synchronized,因为赋值本身是有原子性的,而volatile又保证了可见性,所以就可以保证线程安全了。

    保证此变量对所有线程的可见性:

    当一条线程修改了这个变量的值,新值对于其他线程可以说是可以立即得知的。Java内存模型规定了所有的变量都存储在主内存,每条线程还有自己的工作内存,线程的工作内存保存了该线程使用到的变量在主内存的副本拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接读取主内存中的变量。

    知识拓展:内存可见性

    • 概念:JVM内存模型:主内存 和 线程独立的 工作内存。Java内存模型规定,对于多个线程共享的变量,存储在主内存当中,每个线程都有自己独立的工作内存(比如CPU的寄存器),线程只能访问自己的工作内存,不可以访问其它线程的工作内存。工作内存中保存了主内存共享变量的副本,线程要操作这些共享变量,只能通过操作工作内存中的副本来实现,操作完毕之后再同步回到主内存当中。
    • 如何保证多个线程操作主内存的数据完整性是一个难题,Java内存模型也规定了工作内存与主内存之间交互的协议,定义了8种原子操作:
      • lock:将主内存中的变量锁定,为一个线程所独占。
      • unclock:将lock加的锁定解除,此时其它的线程可以有机会访问此变量。
      • read:将主内存中的变量值读到工作内存当中。
      •  load:将read读取的值保存到工作内存中的变量副本中。
      • use:将值传递给线程的代码执行引擎。
      • assign:将执行引擎处理返回的值重新赋值给变量副本。
      • store:将变量副本的值存储到主内存中。
      • write:将store存储的值写入到主内存的共享变量当中。

    通过上面Java内存模型的概述,我们会注意到这么一个问题,每个线程在获取锁之后会在自己的工作内存来操作共享变量,操作完成之后将工作内存中的副本回写到主内存,并且在其它线程从主内存将变量同步回自己的工作内存之前,共享变量的改变对其是不可见的。

    即其他线程的本地内存中的变量已经是过时的,并不是更新后的值。volatile保证可见性的原理是在每次访问变量时都会进行一次刷新,因此每次访问都是主内存中最新的版本。所以volatile关键字的作用之一就是保证变量修改的实时可见性。

    即,volatile的特殊规则就是:

    • read、load、use动作必须连续出现。
    • assign、store、write动作必须连续出现。

    所以,使用volatile变量能够保证:

    • 每次读取前必须先从主内存刷新最新的值。
    • 每次写入后必须立即同步回主内存当中。

    也就是说,volatile关键字修饰的变量看到的是自己的最新值。线程1中对变量v的最新修改,对线程2是可见的。

    禁止指令重排序优化:

    volatile boolean isOK = false;
    
    //假设以下代码在线程A执行
    A.init();
    isOK=true;
    
    //假设以下代码在线程B执行
    while(!isOK){
      sleep();
    }
    B.init();

    A线程在初始化的时候,B线程处于睡眠状态,等待A线程完成初始化的时候才能够进行自己的初始化。这里的先后关系依赖于isOK这个变量。

    如果没有volatile修饰isOK这个变量,那么isOK的赋值就可能出现在A.init()之前(指令重排序,Java虚拟机的一种优化措施),此时A没有初始化,而B的初始化就破坏了它们之前形成的那种依赖关系,可能就会出错。

    知识拓展:指令重排序

    • 概念:指令重排序是JVM为了优化指令,提高程序运行效率,在不影响 单线程程序 执行结果的前提下,尽可能地提高并行度。编译器、处理器也遵循这样一个目标。注意是单线程。多线程的情况下指令重排序就会给程序带来问题。

    不同的指令间可能存在数据依赖。比如下面的语句:

      int l = 3; // (1)
      int w = 4; // (2)
      int s = l * w; // (3)

    面积的计算依赖于l与w两个变量的赋值指令。而l与w无依赖关系。

    重排序会遵守两个规则:

    • as-if-serial规则:as-if-serial规则是指不管如何重排序(编译器与处理器为了提高并行度),(单线程)程序的结果不能被改变。这是编译器、Runtime、处理器必须遵守的语义。
    • happens-before规则
      • 程序顺序规则:一个线程中的每个操作,happens-before于线程中的任意后续操作。
      • 监视器锁规则一个锁的解锁,happens-before于随后对这个锁的加锁。
      • volatile变量规则:对一个volatile域的写,happens-before于任意后续对这个volatile域的读。
      • 传递性:如果(A)happens-before(B),且(B)happens-before(C),那么(A)happens-before(C)。
      • 线程start()规则:主线程A启动线程B,线程B中可以看到主线程启动B之前的操作。也就是start() happens-before 线程B中的操作。
      • 线程join()规则:主线程A等待子线程B完成,当子线程B执行完毕后,主线程A可以看到线程B的所有操作。也就是说,子线程B中的任意操作,happens-before join()的返回。
      • 中断规则:一个线程调用另一个线程的interrupt,happens-before于被中断的线程发现中断。
      • 终结规则:一个对象的构造函数的结束,happens-before于这个对象finalizer的开始。
      • 概念:前一个操作的结果可以被后续的操作获取。讲直白点就是前面一个操作把变量a赋值为1,那后面一个操作肯定能知道a已经变成了1。
      • happens-before(先行发生)规则如下:

    虽然,(1)-happensbefore ->(2),(2)-happens before->(3),但是计算顺序(1)(2)(3)与(2)(1)(3)对于l、w、area变量的结果并无区别。编译器、Runtime在优化时可以根据情况重排序(1)与(2),而丝毫不影响程序的结果。

    • volatile使用场景:
      • 1、对变量的写操作不依赖当前变量的值。
      • 2、该变量没有包含在其他变量的不变式中。
      • 如果正确使用volatile的话,必须依赖下以下种条件:

    也可以这样理解,就是上面的2个条件需要保证操作是原子性操作,才能保证使用volatile关键字的程序在并发时能够正确执行。

    第一个条件的限制使 volatile 变量不能用作线程安全计数器。虽然增量操作(i++)看上去类似一个单独操作,实际上它是一个由(读取-修改-写入)操作序列组成的组合操作,必须以原子方式执行,而 volatile 不能提供必须的原子特性。

    实现正确的操作需要使 i 的值在操作期间保持不变,而 volatile 变量无法实现这点。

    • 在以下两种情况下都必须使用volatile:
      • 1、状态的改变。
      • 2、读多写少的情况。

    具体如下:

    // 场景一:状态改变
    
    /**
     * 双重检查(DCL)
     */
    public class Sun {
      private static volatile Sun sunInstance;
    
      private Sun() {
      }
    
      public static Sun getSunInstance() {
        if (sunInstance == null) {
          synchronized (Sun.class) {
            if (sunInstance == null){
              sunInstance = new Sun();
            }
          }
        }
        return sunInstance;
      }
    }
    
    // 场景二:读多写少
    
    public class VolatileTest {
        private volatile int value;
    
        //读操作,没有synchronized,提高性能
        public int getValue() {
            return value;
        }
    
        //写操作,必须synchronized。因为x++不是原子操作
        public synchronized int increment() {
            return value++;
        }
    }

    问题来了,volatile是如何防止指令重排序优化的呢?

    答:

    volatile关键字通过 “内存屏障” 的方式来防止指令被重排序,为了实现volatile的内存语义,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。大多数的处理器都支持内存屏障的指令。

    对于编译器来说,发现一个最优布置来最小化插入屏障的总数几乎不可能,为此,Java内存模型采取保守策略。下面是基于保守策略的JMM内存屏障插入策略:

    • 在每个volatile写操作的前面插入一个StoreStore屏障。
    • 在每个volatile写操作的后面插入一个StoreLoad屏障。
    • 在每个volatile读操作的后面插入一个LoadLoad屏障。
    • 在每个volatile读操作的后面插入一个LoadStore屏障。

    知识拓展:内存屏障

    内存屏障(Memory Barrier,或有时叫做内存栅栏,Memory Fence)是一种CPU指令,用于控制特定条件下的重排序和内存可见性问题。Java编译器也会根据内存屏障的规则禁止重排序。

    内存屏障可以被分为以下几种类型:

    • LoadLoad屏障:对于这样的语句Load1; LoadLoad; Load2,在Load2及后续读取操作要读取的数据被访问前,保证Load1要读取的数据被读取完毕。
    • StoreStore屏障:对于这样的语句Store1; StoreStore; Store2,在Store2及后续写入操作执行前,保证Store1的写入操作对其它处理器可见。
    • LoadStore屏障:对于这样的语句Load1; LoadStore; Store2,在Store2及后续写入操作被刷出前,保证Load1要读取的数据被读取完毕。
    • StoreLoad屏障:对于这样的语句Store1; StoreLoad; Load2,在Load2及后续所有读取操作执行前,保证Store1的写入对所有处理器可见。它的开销是四种屏障中最大的。在大多数处理器的实现中,这个屏障是个万能屏障,兼具其它三种内存屏障的功能。
  • 相关阅读:
    JavaScript对原始数据类型的拆装箱操作
    Javascript继承(原始写法,非es6 class)
    动态作用域与词法作用域
    自行车的保养
    探索JS引擎工作原理 (转)
    C语言提高 (7) 第七天 回调函数 预处理函数DEBUG 动态链接库
    C语言提高 (6) 第六天 文件(续) 链表的操作
    C语言提高 (5) 第五天 结构体,结构体对齐 文件
    C语言提高 (4) 第四天 数组与数组作为参数时的数组指针
    C语言提高 (3) 第三天 二级指针的三种模型 栈上指针数组、栈上二维数组、堆上开辟空间
  • 原文地址:https://www.cnblogs.com/javazhiyin/p/13521567.html
Copyright © 2011-2022 走看看