• 并发编程之Java内存模型


    5.1 Java内存模型

    JMM即Java Memory Model,它定义了主存、工作内存抽象概念,底层对应着CPU寄存器、缓存、硬件内存、CPU指令优化等。
    JMM体现在以下几个方面

    • 原子性 - 保证指令不会受到线程上下文切换的影响
    • 可见性 - 保证指令不会受cput缓存的影响
    • 有序性 - 保证指令不会受cpu指令并行优化的影响

    5.2 可见性

    退不出的循环
    先来看一个现象,main线程对run变量的修改对于t线程不可见,导致了t线程无法停止 :
    在这里插入图片描述
    为什么呢?分析一下 :
    1.初始状态,t线程刚开始从主内存读取了run的值到工作内存。
    在这里插入图片描述
    2. 因为t线程要频繁从主内存中读取run的值,JIT编译器会将run的值缓存至自己工作内存中的高速缓存中,减少对主存中run的访问,提高效率
    在这里插入图片描述
    3. 1秒之后,main线程修改了run的值,并同步至主存,而t是从自己工作内存中的高速缓存中读取这个变量的值,结果永远是旧值
    在这里插入图片描述
    解决方法
    volatile(易变关键字)
    它可以用来修饰成员变量和静态成员变量,它可以避免线程从自己的工作缓存中查找变量的值,必须到主存中获取它的值,线程操作volatile变量都是直接操作主存。
    可见性 VS 原子性
    前面例子体现的实际就是可见性,它保证的是在多个线程之间,一个线程对volatile变量的修改对另一个线程可见,不能保证原子性,仅用在一个写线程,多个读线程的情况 :
    上例从字节码理解是这样的 :
    在这里插入图片描述
    比较一下之前我们将线程安全时举的例子 :两个线程一个i++ 一个i–,只能保证看到最新值,不能解决指令交错
    在这里插入图片描述
    注意
    synchronized语句块既可以保证代码块的原子性,也同时保证代码块内变量的可见性。但缺点是synchronized是属于重量级操作,性能相对更低。
    如果在前面示例中的死循环中加入System.out.println()会发现即使不加volatile修饰符,线程t也能正确看到对run变量的修改了,想一想为什么?

    5.3 有序性

    JVM会在不影响正确性的前提下,可以调整语句的执行顺序 :
    在这里插入图片描述
    可以看到,至于是先执行i还是先执行j,对最终的结果不会产生影响。所以,上面代码真正执行时,既可以是
    在这里插入图片描述
    也可以是
    在这里插入图片描述
    这种特性称之为指令重排,多线程下指令重排会影响正确性。

    volatile原理

    volatile的底层实现原理是内存屏障,Memory Barrier(Memory Fence)

    • 对volatile变量的写指令后后加入写屏障
    • 对volatile变量的读指令前会加入读屏障
    1. 如何保证可见性
    • 写屏障(sfence)保证在该屏障之前的,对共享变量的改动,都同步到主存当中
      在这里插入图片描述
    • 而读屏障(lfence)保证在该屏障之后,对共享变量的读取,加载的是主存中最新的数据
      在这里插入图片描述
      在这里插入图片描述
      2.如何保证有序性
    • 写屏障会确保指令重排序时,不会将写屏障之前的代码排在写屏障之后
      在这里插入图片描述
    • 读屏障会确保指令重排序时,不会将读屏障之后的代码排在读屏障之前
      在这里插入图片描述
      在这里插入图片描述
      不能解决指令交错 :
    • 写屏障仅仅是保证之后的读能够读到最新的结果,但不能保证读跑到它前面去
    • 而有序性的保证也只是保证了本线程内相关代码不被重排序
      在这里插入图片描述

    double-checked locking 单例模式为例

    在这里插入图片描述
    以上的实现特点是 :

    • 懒惰实例化
    • 首次使用getInstance()才使用synchronized加锁,后续使用时无需加锁
    • 有隐含的,但很关键的一点 : 第一个if使用了INSTANCE变量,是在同步块之外
      但在多线程环境下,上面的代码是有问题的,getInstance方法对应的字节码为 :
      在这里插入图片描述
      其中
    • 17表示创建对象,将对象引用入栈 // new Singleton
    • 20表示复制一份对象引用 // 引用地址
    • 21表示利用一个对象引用,调用构造方法 // 根据引用地址调用
    • 24表示利用一个对象引用,赋值给 static INSTANCE
      也许jvm会优化为 : 先执行24,再执行21.如果两个线程t1,t2按如下时间序列执行 :
      在这里插入图片描述
      关键在于 0 :getstatic这行代码在monitor控制之外,它就像之前举例中不守规则的人,可以越过monitor读取INSTANCE变量的值
      这时t1还未完成将构造方法执行完毕,如果在构造方法中要执行很多初始化操作,那么t2拿到的是将是一个未初始化完毕的单例
      对INSTANCE使用volatile修饰即可,可以禁用指令重排,但要注意在JDK5以上版本的volatile才会真正有效

    4.double-checked locking 解决

    在这里插入图片描述
    字节码上看不出来volatile指令的效果
    在这里插入图片描述
    在这里插入图片描述
    happens-before
    happens-before规定了对共享变量的写操作对其它线程的读操作可见,它是可见性与有序性的一套规则总结,抛开以下happens-before规则,JMM并不能保证一个线程对共享变量的写,对于其它线程对该共享变量的读可见

    • 线程解锁m之前对变量的写,对于接下来对m加锁的其它线程对该变量的读可见
      在这里插入图片描述
    • 线程对volatile变量的写,对接下来其它线程对该变量的读可见
      在这里插入图片描述
    • 线程start前对变量的写,对该线程开始后对该变量的读可见
      在这里插入图片描述
    • 线程结束前对变量的写,对其它线程得知它结束后的读可见(比如其它线程调用t1.isAlive()或t1.join()等待它结束)
      在这里插入图片描述
    • 线程t1打断t2(interrupt)前对变量的写,对于其他线程得知t2被打断后对变量的读可见(通过t2.interrupted或t2.interrupted)
      在这里插入图片描述
    • 对变量默认值(0,false,null)的写,对其它线程对该变量的读可见
    • 具有传递性,如果x hb -> z 那么有x hb -> z,配合volatile的防指令重排,有下面的例子
      在这里插入图片描述
      变量都是指成员变量或静态成员变量
  • 相关阅读:
    【转载】《代码大全2》读书笔记之…
    【转载】使用注解和反射实现通用性…
    【转载】使用注解和反射实现通用性…
    23种设计模式的形象比喻 (转载)
    SSH 整合- 3 - add - hibernate
    SSH 整合- 4 - add service_servic…
    SSH 整合- 5 - service_serviceImp…
    SSH 整合- 6 - service_serviceImp…
    SSH 整合- 6 - service_serviceImp…
    Hadoop-2.6.0 集群的安装配置
  • 原文地址:https://www.cnblogs.com/haizai/p/12310726.html
Copyright © 2020-2023  润新知