0%

Java内存模型JMM

  • JMM 体现在以下几个方面:
    • 原子性:保证指令不会受到线程上下文切换的影响
    • 可见性:保证指令不会受 cpu 缓存的影响
    • 有序性:保证指令不会受 cpu 指令并行优化的影响

Java内存模型JMM

  • Java 内存模型是 Java Memory Model(JMM),本身是一种抽象的概念,实际上并不存在,描述的是一组规则或规范,通过这组规范定义了程序中各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式。

    JMM 作用:

    • 屏蔽各种硬件和操作系统的内存访问差异,实现让 Java 程序在各种平台下都能达到一致的内存访问效果
    • 规定了线程和内存之间的一些关系
  • JMM 体现在以下几个方面:

    • 原子性:保证指令不会受到线程上下文切换的影响
    • 可见性:保证指令不会受 cpu 缓存的影响
    • 有序性:保证指令不会受 cpu 指令并行优化的影响
  • 根据 JMM 的设计,系统存在一个主内存(Main Memory),Java 中所有变量都存储在主存中,对于所有线程都是共享的;每条线程都有自己的工作内存(Working Memory),工作内存中保存的是主存中某些变量的拷贝,线程对所有变量的操作都是先对变量进行拷贝,然后在工作内存中进行,不能直接操作主内存中的变量;线程之间无法相互直接访问,线程间的通信(传递)必须通过主内存来完成。

    img

    • 主内存和工作内存:
      • 主内存:计算机的内存,也就是经常提到的 8G 内存,16G 内存,存储所有共享变量的值
      • 工作内存:存储该线程使用到的共享变量在主内存的的值的副本拷贝
  • JVM 和 JMM 之间的关系:JMM 中的主内存、工作内存与 JVM 中的 Java 堆、栈、方法区等并不是同一个层次的内存划分,这两者基本上是没有关系的,如果两者一定要勉强对应起来:

    • 主内存主要对应于 Java 堆中的对象实例数据部分,而工作内存则对应于虚拟机栈中的部分区域
    • 从更低层次上说,主内存直接对应于物理硬件的内存,工作内存对应寄存器和高速缓存

原子性

  • 原子性:不可分割,完整性,也就是说某个线程正在做某个具体业务时,中间不可以被分割,需要具体完成,要么同时成功,要么同时失败,保证指令不会受到线程上下文切换的影响。
  • 定义原子操作的使用规则:
    1. 一个变量在同一时刻只允许一条线程对其进行 lock 操作,但 lock 操作可以被同一线程重复执行多次,多次执行 lock 后,只有执行相同次数的 unlock 操作,变量才会被解锁,lock 和 unlock 必须成对出现
    2. 如果对一个变量执行 lock 操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量之前需要重新从主存加载
    3. 如果一个变量事先没有被 lock 操作锁定,则不允许执行 unlock 操作,也不允许去 unlock 一个被其他线程锁定的变量
    4. 对一个变量执行 unlock 操作之前,必须先把此变量同步到主内存中(执行 store 和 write 操作)

可见性

  • 可见性:是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值

    • 存在不可见问题的根本原因是由于缓存的存在,线程持有的是共享变量的副本,无法感知其他线程对于共享变量的更改,导致读取的值不是最新的。但是 final 修饰的变量是不可变的,就算有缓存,也不会存在不可见的问题

    • main 线程对 run 变量的修改对于 t 线程不可见,导致了 t 线程无法停止:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      static boolean run = true;	//添加volatile
      public static void main(String[] args) throws InterruptedException {
      Thread t = new Thread(()->{
      while(run){
      // ....
      }
      });
      t.start();
      sleep(1);
      run = false; // 线程t不会如预想的停下来
      }
      • 原因:
        • 初始状态, t 线程刚开始从主内存读取了 run 的值到工作内存
        • 因为 t 线程要频繁从主内存中读取 run 的值,JIT 编译器会将 run 的值缓存至自己工作内存中的高速缓存中,减少对主存中 run 的访问,提高效率
        • 1 秒之后,main 线程修改了 run 的值,并同步至主存,而 t 是从自己工作内存中的高速缓存中读取这个变量的值,结果永远是旧值

      img

有序性

  • 有序性:在本线程内观察,所有操作都是有序的;在一个线程观察另一个线程,所有操作都是无序的,无序是因为发生了指令重排序

  • CPU 的基本工作是执行存储的指令序列,即程序,程序的执行过程实际上是不断地取出指令、分析指令、执行指令的过程,为了提高性能,编译器和处理器会对指令重排,一般分为以下三种:

    1
    源代码 -> 编译器优化的重排 -> 指令并行的重排 -> 内存系统的重排 -> 最终执行指令

    现代 CPU 支持多级指令流水线,几乎所有的冯 • 诺伊曼型计算机的 CPU,其工作都可以分为 5 个阶段:取指令、指令译码、执行指令、访存取数和结果写回,可以称之为五级指令流水线。CPU 可以在一个时钟周期内,同时运行五条指令的不同阶段(每个线程不同的阶段),本质上流水线技术并不能缩短单条指令的执行时间,但变相地提高了指令地吞吐率

    • 处理器在进行重排序时,必须要考虑指令之间的数据依赖性
      • 单线程环境也存在指令重排,由于存在依赖性,最终执行结果和代码顺序的结果一致
      • 多线程环境中线程交替执行,由于编译器优化重排,会获取其他线程处在不同阶段的指令同时执行
---------------The End---------------