Java内存模型

122 阅读4分钟

1、Java为什么要有内存模型?

计算机硬件CUP多核+多级缓存,多线程时缓存一致性问题,在多线程并发过程中,如何处理多线程读同步问题与可见性(多线程缓存与指令重排序)、多线程写同步问题与原子性(多线程竞争racecondition)。

2、Java内存模型(JMM)

工作内存+主存的模式。JMM是JVM一个规范,用来屏蔽不同硬件和操作系统访问内存的差异,使得Java语言能够统一访问内存。JMM规范了Java虚拟机与计算机内存是如何协同工作的:规定了一个线程如何和何时可以看到由其他线程修改过后的共享变量的值,以及在必须时如何同步的访问共享变量。

3、as-if-serial语义:

不管怎么重排序(编译器和处理器为了提高并行度),(单线程)程序的执行结果不能被改变。(编译器、runtime和处理器都必须遵守as-if-serial语义)

4、Happen-Before关系:指前一个操作的执行结果必须对后一个操作可见。

JMM为程序中所有的操作定义了一个偏序关系,称之为Happens-Before。

  1. 程序顺序规则:如果程序中操作A在操作B之前,那么在线程中操作A将在操作B之前执行。
  2. 监视器锁规则:在监视器锁上的解锁操作必须在同一个监视器锁上的加锁操作之前执行。volatile变量规则:对volatile变量的写入操作必须在对该变量的读操作之前执行。
  3. 线程启动规则:在线程上对 Thread.start 的调用必须在该线程中执行任何操作之前执行。
  4. 线程结束规则:线程中的任何操作都必须在其他线程检测到该线程已经结束之前执行,或者从 Thread.join 中成功返回,或者在调用 Thread.isAlive 时返回 false。
  5. 中断规则:当一个线程在另一个线程上调用 interrupt 时,必须在被中断线程检测到 interrupt 调用之前执行(通过抛出 InterruptException,或者调用 isInterrupted 和 interrupted)。
  6. 终结器规则:对象的构造函数必须在启动该对象的终结器之前执行完成。
  7. 传递性:如果操作 A 在操作 B 之前执行,并且操作 B 在操作 C 之前执行,那么操作 A 必须在操作 C 之前执行。

5、重排序:会导致并发线程安全信息问题(可见性),必须通过JMM规范来约束(as-if-serial语义、Happen-Before关系)。

在执行程序时为了提高性能,编译器和处理器常常会对指令做重排序。重排序分三类:

  1. 编译器优化的重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。
  2. 指令级并行的重排序。现代处理器采用了指令级并行技术(Instruction-LevelParallelism,ILP)来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。
  3. 内存系统的重排序。由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。
    上面的这些重排序都可能导致多线程程序出现内存可见性问题。对于编译器,JMM的编译器重排序规则会禁止特定类型的编译器重排序(不是所有的编译器重排序都要禁止)。对于处理器重排序,JMM 的处理器重排序规则会要求Java 编译器在生成指令序列时, 插入特定类型的内存屏障指令,通过内存屏障指令来禁止特定类型的处理器重排序(不是所有的处理器重排序都要禁止)

6、volatile 写-读的内存定义

  • 当写一个 volatile 变量时,JMM 会把该线程对应的本地内存中的共享变量值刷新到主内存。
  • 当读一个 volatile 变量时,JMM 会把该线程对应的本地内存置为无效。线程接下来将从主内存中读取共享变量。

JMM相关总结:

  • 定义:Java 内存模型通过定义了一系列的 happens-before 操作,让应用程序开发者能够轻易地表达不同线程的操作之间的内存可见性。

  • 在遵守 Java 内存模型的前提下,即时编译器以及底层体系架构能够调整内存访问操作,以达到性能优化的效果。如果开发者没有正确地利用 happens-before 规则,那么将可能导致数据竞争。

  • Java 内存模型是通过内存屏障来禁止重排序的。对于即时编译器来说,内存屏障将限制它所能做的重排序优化。对于处理器来说,内存屏障会导致缓存的刷新操作。