Freeman's Blog

一个菜鸡心血来潮搭建的个人博客

0%

Java内存模型(JMM)

从CPU-寄存器-高速缓存-内存说起

  • 问题:CPU不可能只靠寄存器来完成运算任务,但CPU的速度和内存的存取速度相差太远。
  • 解决:引入高速缓存来缓和CPU速度和内存存取速度的差异。CPU直接独写高速缓存,运算结束再将数据从缓存同步回内存中。
  • 新的问题:缓存一致性。多个CPU有自己的高速缓存,但是他们共享同一个内存,如果多个CPU的运算涉及同一个内存区域将产生数据的不一致问题。此时需要引入缓存一致性协议。
    • 处理器对内存的读写操作的执行顺序,不一定与内存实际发生的独写顺序一致。

Java内存模型

  • 一组规范,并不真实存在,定义了一组规则,定义了程序中各个变量的访问方式(确认一段程序的执行轨迹是否是合法的)。
  • 检查程序执行轨迹的每个读操作,根据一些规则检查读操作观察到的写操作是否是有效的。
  • JMM的实现可以是任意的,只要生成的代码的执行结果能够根据JMM的规则进行预测即可(松规范,给编译器/JVM实现者进行优化的空间,比如指令重排序或者移除不必要的同步)。

JMM的规定

  • 所有共享变量(不包括局部变量,局部变量线程私有)都存储于主内存。共享变量:实例变量、类变量(静态字段)、数组元素。
  • 每一个线程还在自己的工作内存中保留了被线程使用的变量的工作副本。
  • 线程对变量的所有的操作(读,取)都必须在工作内存中完成,而不能直接读写主内存中的变量。
  • 不同线程之间也不能直接访问对方工作内存中的变量,线程间变量的值的传递需要通过主内存中转来完成。
  • intra-thread semantics: 保证重排序不会改变单线程内的程序执行结果。
  • 对指令的重排序:
    • as-if-serial:不能改变单线程程序的语义,重排序后单线程程序的执行结果不能被改变。
    • 不存在数据依赖性的情况下,处理器可以进行重排序
      • 两个操作访问同一个变量,且其中一个操作为写操作,此时两个操作之间存在依赖性。

Java内存模型的三大特性(JMM在多线程环境下的三个问题)

  1. 可见性:一个线程修改了共享变量的值,其他线程能够立即得知这个修改。而单纯遵照JMM的规定,多线程程序中单个线程并不一定能马上获知其它线程对共享变量的更改。JMM通过在变量修改后将值同步回主内存,在变量读取前从主内存刷新变量值来实现可见性。
    • volatile关键字:
    • synchronized关键字:进入synchronized代码块前,线程会获得锁,清空工作内存,从主内存拷贝共享变量最新的值到工作内存成为副本。进入synchronized代码后,会将修改后的副本的值刷新回主内存中,线程才会释放锁。
    • final关键字
  2. 原子性(?):
  3. 有序性
    • 进行指令重排的时候必须考虑数据的依赖性。
    • 在多线程程序中,不同线程之间存在控制依赖关系重排序可能会改变程序的执行结果。
    • 举例:
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      public class ControlDependency {
      int a = 0;
      boolean flag = false;

      public void init() {
      a = 1; // 1
      flag = true; // 2
      }

      public void use() {
      if (flag) { // 3
      int i = a * a; // 4
      }
      }
      }
      在单线程环境下,语句1、2和语句3、4都不存在数据依赖性,因此指令会被重排序。但是在编译器在判断if语句块时会对语句块的内容进行猜测,因此会存在先执行int i = a * a;,然后再判断flag。如果另一线程在执行init时将指令重排序,先执行flag = true,然后又发生了线程切换,这时a的值为0,flag的值却为true。这样就会得到错误的结果int i = 0;

怎么解决JMM在多线程环境下的问题

  1. 插入内存屏障禁止指令重排序。(volatile
  2. 设置临界区。(synchronized):JMM允许临界区内的代码重排序,不允许临界区内的代码逃逸到临界区之外。
  3. Happens-Before语义:如果一个操作的执行结果需要对另一个操作可见,那么这两个操作之间必须存在happens-before关系。但这并不意味之前一个操作必须在后一个操作之前执行。Happens-before语义类似于单线程环境下的as-if-serial语义,它保证正确同步的多线程程序执行结果不被改变。
    • 程序顺序规则:一个线程中的每个操作,happens-before于该线程中的任意后续操作
    • 监视器锁规则:对一个锁的解锁,happens-before于随后对这个锁的加锁。
    • volatile变量规则:对一个volatile字段的写操作happens-before于任意后续对这个volatile域的读。
    • 传递性:A happens-before B ,B happens-before C,那么A happens-before C
    • start()规则:如果线程A执行ThreadB.start(),那么A线程的start() happens-before于线程B中的任意操作
    • join()规则:如果线程A执行ThreadB.join(),那么线程B中的任意操作happens-before于线程A从ThreadB.join()成功返回
    • 线程中断规则:对线程interrput方法的调用happens-before于被中断线程的代码检查到中断事件的发生。