#Java#互联网大厂面试必问的Java内存模型以及并发编程( 三 )



JMM 与 JVM 内存结构JMM 与 Java 内存区域中的堆、栈、方法区等并不是同一个层次的内存划分 , 两者基本没有关系 。 如果一定要勉强对应 , 那从变量、主内存、工作内存的定义看 , 主内存主要对应 Java 堆中的对象实例数据部分 , 工作内存则对应虚拟机栈的部分区域(与上图对应着看哈) 。
JMM 与计算机内存结构Java 内存模型和硬件内存体系结构也没有什么关系 。 硬件内存体系结构不区分栈和堆 。 在硬件上 , 线程栈和堆都位于主内存中 。 线程栈和堆的一部分有时可能出现在高速缓存和CPU寄存器中 。 如下图所示:
当对象和变量可以存储在计算机中不同的内存区域时 , 这就可能会出现某些问题 。 两个主要问题是:

  • 线程更新(写)到共享变量的可见性
  • 读取、检查和写入共享变量时的竞争条件
可见性问题(Visibility of Shared Objects)如果两个或多个线程共享一个对象 , 则一个线程对共享对象的更新可能对其他线程不可见(当然可以用 Java 提供的关键字 volatile) 。 假设共享对象最初存储在主内存中 。 在 CPU 1上运行的线程将共享对象读入它的CPU缓存后修改 , 但是还没来得及即刷新回主内存 , 这时其他 CPU 上运行的线程就不会看到共享对象的更改 。 这样 , 每个线程都可能以自己的线程结束 , 就出现了可见性问题 , 如下
竞争条件(Race Conditions)这个其实就是我们常说的原子问题 。
如果两个或多个线程共享一个对象 , 并且多个线程更新该共享对象中的变量 , 则可能出现竞争条件 。
想象一下 , 如果线程A将一个共享对象的变量读入到它的CPU缓存中 。 此时 , 线程B执行相同的操作 , 但是进入不同的CPU缓存 。 现在线程A执行 +1 操作 , 线程B也这样做 。 现在该变量增加了两次 , 在每个CPU缓存中一次 。
如果这些增量是按顺序执行的 , 则变量结果会是3 , 并将原始值+ 2写回主内存 。 但是 , 这两个增量是同时执行的 , 没有适当的同步 。 不管将哪个线程的结构写回主内存 , 更新后的值只比原始值高1 , 显然是有问题的 。 如下(当然可以用 Java 提供的关键字 Synchronized)
JMM 特性JMM 就是用来解决如上问题的 。 JMM是围绕着并发过程中如何处理可见性、原子性和有序性这 3 个 特征建立起来的
  • 可见性:可见性是指当一个线程修改了共享变量的值 , 其他线程能够立即得知这个修改 。 Java 中的 volatile、synchronzied、final 都可以实现可见性
  • 原子性:即一个操作或者多个操作 , 要么全部执行并且执行的过程不会被任何因素打断 , 要么就都不执行 。 即使在多个线程一起执行的时候 , 一个操作一旦开始 , 就不会被其他线程所干扰 。
  • 有序性:计算机在执行程序时 , 为了提高性能 , 编译器和处理器常常会对指令做重排 , 一般分为以下 3 种单线程环境里确保程序最终执行结果和代码顺序执行的结果一致;处理器在进行重排序时必须要考虑指令之间的数据依赖性;多线程环境中线程交替执行 , 由于编译器优化重排的存在 , 两个线程中使用的变量能否保证一致性是无法确定的 , 结果无法预测
内存之间的交互操作关于主内存和工作内存之间具体的交互协议 , 即一个变量如何从主内存拷贝到工作内存、如何从工作内存同步回主内存之类的实现细节 , Java 内存模型中定义了 8 种 操作来完成 , 虚拟机实现必须保证每一种操作都是原子的、不可再拆分的(double和long类型例外)