Java多线程系列 - Volatile

版权声明 :

本文为博主原创文章,如需转载,请注明出处(https://blog.csdn.net/F1004145107/article/details/87952142)

1. 内存模型相关概念

  • 缓存一致性问题 : 多线程序运行时,所有的临时数据都是存放在内存中的,CPU在运行时会从内存中读取数据,然后将数据记载到CPU内部的一个缓存空间中,在运行过程中所有的数据操作都是在CPU内部缓存中进行的,执行结束后将缓存中的数据更新到内存中,这样可以提高CPU的效率,但是一旦当在多线程环境下运行时,此时可能造成第二个线程从内存中读取到的数据是旧的数据,最新的数据还在其他CPU的缓存中,或者其他CPU可能在更新数据时覆盖了其他CPU更新的数据

  • MESI协议 : 当CPU改变了数据时,如果当前操作的变量是共享的话,他会通知其余所有的其余有当前变量副本的CPU告诉他们数据被改变,你们那份过时了,此时其余CPU就回将当前变量副本置位无效状态,当用到该变量时会再次去内存中读取数据

2.Volatile的特性

  • 原子性 :

    • 在Java的官网文档中关于原子性的描述

      • 1.Reads and writes are atomic for reference variables and for most primitive variables (all types except long and double).

      • 2.Reads and writes are atomic for all variables declared volatile(including longand doublevariables).

      第一句句话说的是'读取和写入对于引用变量和大多数原始变量读取和写入来说都是原子性的,除了long和double'

      但是第二句话也说了,'当该变量使用volatile修饰的时候,所有的变量读取和写入都是原子性的,包括long和double'

    • int num = 0; 这个操作是原子性的

      扫描二维码关注公众号,回复: 11218103 查看本文章
    • num ++; 这个操作并不是原子性的,这个操作的步骤是

      • 1.将num副本加入到操作数栈

      • 2.进行+1操作

      • 3.弹出操作数栈

    • 总结 : Volatile只能保证被修饰的变量的读取和写入是原子性的,但是并不能保证非原子性的操作是原子性,例如num ++;

  • 可见性

    • Volatile能保证当有一个线程修改了该变量的值时,该值会被立马同步到主内存中,并通知其余线程该变量已更新,例如如果有一个线程A已经将变量副本压入到操作数栈中,哪怕线程A被阻塞了,线程B修改了该变量,此时即使已经通知到了线程A,但是因为线程A中变量已经被压入操作数栈了,线程A不会再次去主内存中去读取新的值,所以变量任然是A线程最初时读取到的值

  • 有序性

    ​ JVM在实际运行中可能会为了提高性能而对代码的顺序进行重排序,当然重排序的的执行结果与顺序执行的结果是一致的

    1  int a = 0;
    2  int b = 1;
    3  a = 2;
    4  b = 3;
    //上述代码3,4即时重排序了也不会影响到最终结果
    ​
    1  int a = 1;
    2  int b = 2;
    3  a = 4;
    4  b = a;
    //上述代码3,4是不会被重排序的,因为第4行代码对3是有依赖关系的       

    ​ 正常创建一个对象的过程是

    ​ 1.先在堆中分配空间

    ​ 2.在栈中创建对象变量,

    ​ 3.最后将对象的引用地址值存入到栈中的变量中

​ 在单线程下创建对象的顺序即时被打乱了也不会造成影响,但如果是在并发情况下,很有可能A线程先

​ 完成了1,2步骤,此时A线程被阻塞,B线程开始执行,B线程此时拿到的对象已经可能不为null了,但是并没

​ 有完成初始化,此时B线程拿到这个对象进行操作的话就一定会报错

  • 原理

    观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令 ----《深入理解Java虚拟机》

    lock指令的意义相当于一个内存屏障,在整个代码块中屏蔽了JVM对其重排序,这个是Volatile实现有序性以及可见性的重要原因,

    • Volatile保证了该在其之前执行的代码不会跑到该行之后执行,也就是说在执行到内存屏障之前,该在它之前执行完毕的代码已全部都执行完了,可以避免多线程并发下的问题

    • 强制将对CPU线程内部缓存的修改同步到主内存中

    • 通知其余CPU线程缓存的中的副本失效

  • 应用场景

    • 做标记

      因为Volatile无法保证非原子操作的原子性,所以它不能当写锁,但是可以当读锁

      private volatile boolean isSuccess = false;
      ​
      public static void say () {
          while (isSuccess) {
              System.out.printf("isSuccess : %s" ,isSuccess);
          }
      }
      ​
      public void setIsSuccess (boolean isSuccess) {
          this.isSuccess = isSuccess;
      }

      该代码中,即时多个线程并发,但是可以保证isSuccess

    • public class DoubleCheck {
          private volatile static DoubleCheck doubleCheck = null;
          private DoubleCheck () {}
          
          public static DoubleCheck getInstance () {
              if (doubleCheck == null) {
                  synchronized (DoubleCheck.class) {
                      if (doubleCheck == null) {
                          doubleCheck = new DoubleCheck();
                      }
                  }
              }
              return doubleCheck;
          }
      }
原创文章 42 获赞 51 访问量 1万+

猜你喜欢

转载自blog.csdn.net/F1004145107/article/details/87952142