• 探讨 volatile 关键字


    目录

    引入:

    1、volatile含义

    2、volatile三个特性:

    (1、保证可见性

    Java的内存模型JMM(Java Memory Model)

    (2、不保证原子性

    (3、禁止指令重排序

    单例模式的双重锁中要加volatile


    引入:

    为什么使用 volatile关键字呢?给大家举个例子吧!!!

    很大的一个原因就是关于编译器自动优化的问题,看下面一段代码:

    1. class Counter{
    2. public static int count;
    3. }
    4. public class Test {
    5. public static void main(String[] args) {
    6. Thread t1 = new Thread(()-> {
    7. while(Counter.count==0) {
    8. }
    9. System.out.println("t1线程结束");
    10. });
    11. Thread t2 = new Thread(()-> {
    12. System.out.println("输入一个数:");
    13. Scanner scanner = new Scanner(System.in);
    14. Counter.count = scanner.nextInt();
    15. });
    16. t1.start();
    17. t2.start();
    18. }
    19. }

             以上代码,当你执行时,你会发现你输入一个数,改变了count的值,但代码依旧没停止,还在while循环中执行着,出现这个问题的原因就是编译器的自动优化,到底是怎么优化的?

            首先每进行while条件的比较时,都会读取内存中count的值,将其加载到CPU寄存器中,再进行计算比较处理。而在此时,编译器会以为没有人再去修改count的值,而读取内存加载到寄存器这又是相对比较大的开销(与计算比较处理相比),所以编译器在这里自动优化,省去了读取内存这一步骤,直接取寄存器的数据进行比较。

            这里需要注意的是,编译器自动优化这件事,对于我们这些没有编写过JVM的程序员来说,算是一个未知的东西,例如上述代码我们在循环里面加上代码:

    1. try {
    2. Thread.sleep(500);
    3. } catch (InterruptedException e) {
    4. throw new RuntimeException(e);
    5. }

    我们再次运行时,编译器又没有对其进行优化了 

    1、volatile含义

            volatile是Java提供的一种轻量级的同步机制。Java 语言包含两种内在的同步机制:同步块(或方法)和 volatile 变量,相比于synchronized(synchronized通常称为重量级锁),volatile更轻量级,因为它不会引起线程上下文的切换和调度。但是volatile 变量的同步性较差(有时它更简单并且开销更低),而且其使用也更容易出错。

            volatile可以保证线程可见性且提供了一定的有序性,但是无法保证原子性。在JVM底层volatile是采用“内存屏障”来实现的。观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令,lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:

    • 它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;
    • 它会强制将对缓存的修改操作立即写入主存;
    • 如果是写操作,它会导致其他CPU中对应的缓存行无效。
       

    2、volatile三个特性:

     关于这三个特性是什么意思,这篇博客里面有说明:http://t.csdn.cn/LN8qP 

    (1、保证可见性

    1. //保证内存可见性
    2. class Counter{
    3. public static volatile int count;
    4. }
    5. public class Test1 {
    6. public static void main(String[] args) {
    7. Thread t1 = new Thread(() -> {
    8. while (Counter.count == 0) {
    9. }
    10. System.out.println("t1线程结束");
    11. });
    12. Thread t2 = new Thread(() -> {
    13. System.out.println("输入一个数:");
    14. Scanner scanner = new Scanner(System.in);
    15. Counter.count = scanner.nextInt();
    16. });
    17. t1.start();
    18. t2.start();
    19. }
    20. }

    加了关键字之后,输入一个值改变coun值后,循环结束

    Java的内存模型JMM(Java Memory Model)

            volatile禁止了编译器优化,避免了直接读取CPU寄存器中缓存的数据,而是每次都重新读内存。

            用Java的术语来说,应该是:站在JMM的角度看待volatile,正常的程序执行过程中,会把主内存的数据,先加载到工作内存中,再进行计算处理,编译器优化可能会导致不是每次都真的去读取主内存,而直接读取工作内存中的缓存数据(可能导致内存可见性问题),而volatile起到的效果,就是保证每次读取内存都是真的从主存中重新读取

    (2、不保证原子性

    1. //不保证原子性
    2. public class Test2 {
    3. public static volatile int count;
    4. public static void main(String[] args) {
    5. Thread t1 = new Thread(() -> {
    6. while(count<100) {
    7. System.out.println(count++);
    8. try {
    9. Thread.sleep(100);
    10. } catch (InterruptedException e) {
    11. throw new RuntimeException(e);
    12. }
    13. }
    14. });
    15. Thread t2 = new Thread(() -> {
    16. while(count<100) {
    17. System.out.println(count++);
    18. try {
    19. Thread.sleep(100);
    20. } catch (InterruptedException e) {
    21. throw new RuntimeException(e);
    22. }
    23. }
    24. });
    25. t1.start();
    26. t2.start();
    27. }
    28. }

    这里依旧会出现两个线程输出的count的值相同

    (3、禁止指令重排序

    以单例模式举例:

    单例模式的双重锁中要加volatile

    1. public class Singleton {
    2. private volatile static Singleton instance;
    3. private Singleton (){}
    4. public static Singleton getInstance() {
    5. if (instance == null) {
    6. synchronized (Singleton.class) {
    7. if (instance == null) {
    8. instance = new Singleton();
    9. }
    10. }
    11. }
    12. return instance;
    13. }
    14. }

     代码分析:

             双锁模式,进行了两次的判断,第一次是判断是否要加锁,第二次是判断是否要创建实例。由于singleton=new Singleton()对象的创建在JVM中可能会进行重排序,例如:new对象时,大致可以分为三个步骤:

    • 申请内存,得到内存首地址
    • 调用构造方法,初始化实例
    • 把内存的首地址赋值给instance引用

            此时可能会出现编译器自动优化,将步骤2和3调换顺序,而刚好在步骤1、3执行完,步骤2未执行时,另一个线程调用getInstance,这时会认为instance非空,直接返回instance,并且在后续可能会针对instance进行解引用操作,而解决这样的问题,办法就是禁止指令重排 ,使用volatile修饰signleton实例变量有效,解决该问题。

    下期见!!!

  • 相关阅读:
    一次Ambari安装记录
    Shader实战(2):在unity中实现物体材质随时间插值渐变
    Linux软件安装方式 - Tarball&RPM&YUM
    R数据分析:用R建立预测模型
    React hooks介绍及使用
    Jenkins pipeline 自动部署实践
    elasticsearch操作
    Windows 安装 MariaDB 数据库
    盘点ERP开发的那点事-业务流和数据流
    基于Qt4的电机变化数据处理工具开发
  • 原文地址:https://blog.csdn.net/LYJbao/article/details/126899098