美文网首页Java招聘知识合集
【Java并发】1. Java线程内存模型JMM及volatil

【Java并发】1. Java线程内存模型JMM及volatil

作者: 后端技术学习分享 | 来源:发表于2021-04-16 09:56 被阅读0次

    JMM

    并发编程的三大特性:可见性(volatile)、有序性(volatile)、原子性(synchronized)

    • JMM跟CPU缓存模型相似,是基于CPU缓存模型来建立的,是标准化的,屏蔽了不同计算机的区别
    • JMM隶属于JVM,定义了线程与主内存间的抽象关系,线程间的共享变量存放于主内存
    • 每个线程均有私有工作内存(JMM抽象概念,实际不存在),工作内存包含了该线程读写共享变量的副本。如果需要使得变量其他线程可访问,需要加volatile修饰
    • 线程实际操作的数据均为其工作内存的变量副本,所以会出现多线程里相同变量无法同步

    JMM八大原子操作及volatile的可见性原理

    JMM八大原子操作是:lock(锁定)、unlock(解锁)、read(读取)、load(载入)、use(使用)、assign(赋值)、store(存储)、write(写入)。
    有如下案例,当flag有volatile修饰时,执行main函数,将输出"flag值已改";无volatile修饰时则A线程一直死循环

    public class Demo {
      // 没有volatile修饰时,B线程对flag的改动,A线程是不可见的
      // 有volatile修饰时,B线程对flag的改动对A线程同样可见
      private static boolean flag = false;
      public static void setFlag(){
        flag = true;
      }
    
      public static void main(String[] args){
        // A 线程,死循环检查flag的值,如果发生改变,则退出死循环
        new Thread(() -> {
          while(!flag);
          System.out.println("flag值已改");
        }).start();
        // B 线程,改变flag的值
        new Thread(() -> {
          setFlag();
        }).start();
      }
    }
    
    • 在不加volatile修饰时,线程A的JMM操作流程

      • 主内存里有共享变量flag=false
      • read操作:将变量从主内存里读取到线程的工作内存中
      • load操作:将read读取的值从工作内存放到工作内存里的副本变量中
      • use操作:将副本变量传递给cpu使用(例子里A线程需要对falg进行取反操作,即使用use从副本变量里取得flag到cpu,再进行取反计算)
      • 由于A线程在死循环里,所以每次循环检查flag的值时,均会直接从副本变量里取得flag值
    • 在不加volatile修饰时,线程B的JMM操作流程

      • 主内存里有共享变量flag=false
      • read操作:将变量从主内存里读取到线程的工作内存中
      • load操作:将read读取的值从工作内存放到工作内存里的副本变量中
      • use操作:将副本变量传递给cpu使用
      • assign操作:B线程将flag设置为了true,此时触发assign操作,将cpu计算所得值赋值给工作内存里的变量副本中
      • store操作:将工作内存里的共享变量传入主内存
      • write操作:将store传送的值写道主内存变量里,至此主内存内flag=true
      • 所以B线程对共享变量的修改,无法同步到A线程
    图片来自网络
    • volatile实现共享变量可见的原理
      • 了解JVM的都知道volatile具备两个特殊规则:

        • read、load、use动作必须连续出现
          这三个操作将数据从主内存读取到cpu
        • assign、store、write动作必须连续出现
          这三个操作将数据从cpu写回到主内存,也即是赋值语句会马上更新到主内存中去
      • 对变量加了volatile指令后,编译成的汇编指令里会给赋值语句加入lock前缀指令。作用如下:

        • 被volatile修饰的变量,在某线程里其数据在工作内存中但凡出现变动,将被立即写回主内存(相当于JMM操作assign、store、write必须连续出现)
        • 开启缓存一致性协议,数据回写主内存的操作会引起其他线程里对应缓存数据立即失效(工作内存里的对应变量失效)。此时其他线程需要重新从主内存读取变量,这样就确保了其他线程读取到了最新的数据
        • 提供内存屏障功能,使lock指令前后的指令不能重新排序(volatile有序性的原理)

    volatile的有序性原理

    • 指令重排序

      在不影响单线程执行结果(多线程不保证)的情况下,计算机为了优化性能,会对机器指令进行重新排序优化。重排序遵循as-if-serial和happens-before原则

      • as-if-serial:重排序不影响单线程执行结果
      • happens-before:定义了一些规则来遵循
    • 对象半初始化问题

      对于双重检查锁单例模式,如下代码,在执行lazy = new Singleton();语句时,是有可能被指令重排序的。假设A线程由于重排序,在未初始化完成的情况下,先给lazy赋值了,恰巧赋值后B线程也执行getInstance方法,获取到了不为null的lazy变量,而这时候A线程却并没有初始化完毕单例对象,则B线程将使用半初始化的单例对象,造成错误。这就是经典的对象半初始化问题
      对于此问题,只需要在单例变量lazy声明时用volatile修饰即可解决,因为volatile禁止指令重排序

      public class Singleton { 
        private volatile static Singleton lazy = null; 
        private Singleton(){} 
        public static Singleton getInstance(){ 
          if(lazy == null){ 
            synchronized (Singleton.class){ 
              if(lazy == null){ 
                //1.分配内存给这个对象 
                //2.初始化对象 
                //3.设置 lazy 指向刚分配的内存地址
                lazy = new Singleton(); 
              } 
            } 
          }
          return lazy; 
        } 
      }
      
    • volatile关键字通过“内存屏障”来防止指令被重排序,内存屏障底层依旧是通过汇编的lock来实现的

      • JMM内存屏障规范

        • LoadLoad:[Load1;LoadLoad;Load2] 保证load1的读操作在load2及后续读操作前执行
        • StoreStore:保证Store1写操作已刷新至主内存,才进行后续的Store操作
        • LoadStore:保证Load1读取结束后,后续的Store才进行
        • StoreLoad:保证Store1写操作已刷新到主内存后,才进行后续的Load操作
      • JVM要求volatile需要执行的内存屏障规范

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

    原创不易,转载请在开头著名文章来源和作者。如果我的文章对您有帮助,请点赞/收藏/关注鼓励支持一下吧❤❤❤❤❤❤

    相关文章

      网友评论

        本文标题:【Java并发】1. Java线程内存模型JMM及volatil

        本文链接:https://www.haomeiwen.com/subject/drnslltx.html