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的写入对所有处理器可见。它的开销是四种屏障中最大的。在大多数处理器的实现中,这个屏障是个万能屏障,兼具其它三种内存屏障的功能。
  • 相关阅读:
    Log4j appender、layout
    EhCache缓存框架的使用
    Log4j rootLogger根配置以及4种日志级别
    开发chrome 插件, background.js中 console log 看不到解决方法
    Windows cmd 长时间不输出新内容 直到按下ctrl + c 取消或者回车的解决办法
    如何查看当前分支从哪个支线创建而来
    C# 获取相对路径的字符串
    解决adobe air sdk打包 apk后自动在包名前面加上air. (有个点)前缀的问题
    sublime text 输入法候选词不跟随光标
    Windows 批处理设置dns ,解决能上qq不能开网页
  • 原文地址:https://www.cnblogs.com/javazhiyin/p/13521567.html
Copyright © 2011-2022 走看看