CPU缓存与Java内存模型

CPU多级缓存

局部性原理:

  1. 时间局部性:如果某个数据被访问,那么在不久的将来它很可能再次被访问;
  2. 空间局部性:如果某个数据被访问,那么它相邻的数据很快也可能被访问。

缓存一致性(MESI)

定义了四种cache life的四种状态:

状态 描述
M(Modified) 这行数据有效,数据被修改了,和内存中的数据不一致,数据只存在于本Cache中。
E(Exclusive) 这行数据有效,数据和内存中的数据一致,数据只存在于本Cache中。
S(Shared) 这行数据有效,数据和内存中的数据一致,数据存在于很多Cache中。
I(Invalid) 这行数据无效。

CPU多级缓存——乱序执行优化

硬件内存架构:


CPU缓存与Java内存模型_第1张图片
通常情况下,当一个CPU需要读取主存时,它会将主存的部分读到CPU缓存中。它甚至可能将缓存中的部分内容读到它的内部寄存器中,然后在寄存器中执行操作。当CPU需要将结果写回到主存中去时,它会将内部寄存器的值刷新到缓存中,然后在某个时间点将值刷新回主存。

一些问题:(多线程环境下尤其)

缓存一致性问题: 当多个处理器的运算任务都涉及同一块主内存区域时,将可能导致各自的缓存数据不一致的情况,如果真的发生这种情况,需要各个处理器访问缓存时都遵循一些协议,在读写时要根据协议来进行操作,这类协议有MSI、MESI等。
指令重排序问题: 为了使得处理器内部的运算单元能尽量被充分利用,处理器可能会对输入代码进行乱序执行(Out-Of-Order Execution)优化,处理器会在计算之后将乱序执行的结果重组,保证该结果与顺序执行的结果是一致的,但并不保证程序中各个语句计算的先后顺序与输入代码中的顺序一致。

Java内存模型(JMM)

JVM内存模型指的是JVM的内存分区;而Java内存模式是一种虚拟机规范。
JVM模型
CPU缓存与Java内存模型_第2张图片

JMM规范了Java虚拟机与计算机内存是如何协同工作的: 规定了一个线程如何和何时可以看到由其他线程修改过后的共享变量的值,以及在必须时如何同步的访问共享变量。
CPU缓存与Java内存模型_第3张图片
Java内存模型(不仅仅是JVM内存分区): 调用栈和本地变量存放在线程栈上,对象存放在堆上。
CPU缓存与Java内存模型_第4张图片

  • 某个本地变量可能是原始变量,始终“呆在”线程栈上
  • 某个本地变量可能是一个对象的引用,对象本身依然放在堆(heap)上,但这个引用放在本地变量上。
  • 对象中如果包含方法,这些方法可能包含本地变量,本地变量依然放在线程栈上,即使对象和包含的方法放在堆(heap)上。
  • 对象的成员变量是跟随对象一起放在heap上的,不论这个对象是原始变量或者引用。
  • 存放在heap上的对象可被所持有的这个对象的线程访问,同时该线程可以访问该对象的成员变量;如果两个线程同时调用该类的方法,访问其成员变量,那么这两个线程都拥有了这个成员变量的私有拷贝。

Java内存模型和硬件内存架构之间的桥接

JVM与硬件内存架构存在差异;
硬件内存架构没有区分线程栈和堆;
对于硬件而言,所有的线程栈和堆都在主内存里,部分线程的栈和堆会存在于CPU寄存器与cache中。

CPU缓存与Java内存模型_第5张图片

抽象角度看,JMM模型中定义了线程与主内存的关系:

  • 线程之间的共享变量存放在主内存中;
  • 每个线程都有一个私有的本地内存(JMM的抽象概念),里面存放了该线程读/写共享变量的拷贝副本;
  • 低层次角度看,主内存就是硬件内存,为了获取更高的运行速度,虚拟机及硬件系统会将工作内存优先存储与寄存器和高速缓存中;
  • JMM中线程的工作内存,是CPU的寄存器和高速缓存的抽象描述;而JVM的静态存储模型(JVM内存模型)只是一种对内存的物理划分而已,它只局限于内存。

JMM模型下的线程间通信:

线程间通信必须要经过主内存。
Java内存模型定义了以下八种操作来完成:

  1. lock(锁定):作用于主内存的变量,把一个变量标识为一条线程独占状态。
  2. unlock(解锁):作用于主内存变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
  3. read(读取):作用于主内存变量,把一个变量值从主内存传输到线程的工作内存中,以便随后的load动作使用
  4. load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中。
  5. use(使用):作用于工作内存的变量,把工作内存中的一个变量值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时将会执行这个操作。
  6. assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋值给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
  7. store(存储):作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中,以便随后的write的操作。
  8. write(写入):作用于主内存的变量,它把store操作从工作内存中一个变量的值传送到主内存的变量中。
    CPU缓存与Java内存模型_第6张图片

Java内存模型还规定了在执行上述八种基本操作时,必须满足如下规则:

  1. 如果要把一个变量从主内存中复制到工作内存,就需要按顺寻地执行read和load操作, 如果把变量从工作内存中同步回主内存中,就要按顺序地执行store和write操作。但Java内存模型只要求上述操作必须按顺序执行,而没有保证必须是连续执行。
  2. 不允许read和load、store和write操作之一单独出现
  3. 不允许一个线程丢弃它的最近assign的操作,即变量在工作内存中改变了之后必须同步到主内存中。
  4. 不允许一个线程无原因地(没有发生过任何assign操作)把数据从工作内存同步回主内存中。
  5. 一个新的变量只能在主内存中诞生,不允许在工作内存中直接使用一个未被初始化(load或assign)的变量。即就是对一个变量实施use和store操作之前,必须先执行过了assign和load操作。
  6. 一个变量在同一时刻只允许一条线程对其进行lock操作,但lock操作可以被同一条线程重复执行多次,多次执行lock后,只有执行相同次数的unlock操作,变量才会被解锁。lock和unlock必须成对出现
  7. 如果对一个变量执行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前需要重新执行load或assign操作初始化变量的值
  8. 如果一个变量事先没有被lock操作锁定,则不允许对它执行unlock操作;也不允许去unlock一个被其他线程锁定的变量。
  9. 对一个变量执行unlock操作之前,必须先把此变量同步到主内存中(执行store和write操作)。

Java内存模型解决的问题

多线程读同步与可见性
可见性(共享对象可见性):线程对共享变量修改的可见性。当一个线程修改了共享变量的值,其他线程能够立刻得知这个修改;

A、线程缓存导致的可见性问题:

跑在左边CPU的线程拷贝这个共享对象到它的CPU缓存中,然后将count变量的值修改为2。这个修改对跑在右边CPU上的其它线程是不可见的,因为修改后的count的值还没有被刷新回主存中去。

CPU缓存与Java内存模型_第7张图片
解决这种可见性问题:

  1. volatile关键字:volatile关键字可以保证直接从主存中读取一个变量,如果这个变量被修改后,总是会被写回到主存中去。普通变量与volatile变量的区别是:volatile的特殊规则保证了新值能立即同步到主内存,以及每个线程在每次使用volatile变量前都立即从主内存刷新。因此我们可以说volatile保证了多线程操作时变量的可见性,而普通变量则不能保证这一点。
  2. synchorized关键字:同步块的可见性是由:
    “如果对一个变量执行lock操作,将会清空工作内存中变量的值,在执行引擎使用这个变量前需要重新执行load或assign操作初始化变量的值”
    “对一个变量执行unlock操作之前,必须先把此变量的值同步到主内存中(执行store 和write操作)
  3. final关键字:final关键字的可见性是指,被final修饰的字段在构造器中一旦被初始化完成,并且构造器没有把“this”的引用传递出去(this引用逃逸是一件很危险的事情,其他线程有可能通过这个引用访问到“初始化了一半”的对象),那么在其他线程就能看见final字段的值(无须同步)
B、重排序导致的可见性问题:

Java程序中天然的有序性可以总结为一句话:
如果在本地线程内观察,所有操作都是有序的(“线程内表现为串行”(Within-Thread As-If-Serial Semantics));
如果在一个线程中观察另一个线程,所有操作都是无序的(“指令重排序”现象和“线程工作内存与主内存同步延迟”现象)。
Java语言提供了volatile和synchronized两个关键字来保证线程之间操作的有序性:
volatile关键字本身就包含了禁止指令重排序的语义
synchronized则是由“一个变量在同一个时刻只允许一条线程对其进行lock操作”这条规则获得的,这个规则决定了持有同一个锁的两个同步块只能串行地进入


作者:Echo_IX

你可能感兴趣的:(Java高并发)