聊聊Hotspot内存屏障如何禁止指令重排

文章目录

  • 1. 引言
  • 2. JVM内存模型(Java Memory Model, JMM)简介
  • III. Hotspot中的内存屏障
  • 4. 示例:volatile变量在Hotspot中如何使用内存屏障
  • 5. 结论
  • 6. 参考文档

聊聊Hotspot内存屏障如何禁止指令重排_第1张图片

1. 引言

内存屏障,也称为内存栅栏、内存栅障或内存栅栏指令,是一种同步原语,用于解决多线程中的指令重排问题。

在现代多核处理器中,为了提高处理效率,处理器和编译器会对指令进行重排。这种优化在单线程环境中通常没有问题,但在多线程环境中可能会导致问题。因为,如果两个线程访问共享变量时,由于指令重排,可能会看到错误或不一致的数据。

这时,我们就需要内存屏障来解决这个问题。内存屏障可以强制处理器在执行内存屏障之前的所有读/写操作都完成,并确保在内存屏障之后的所有读/写操作都在内存屏障之前的操作完成后才开始。换句话说,内存屏障可以防止指令重排,确保指令的执行顺序与程序的顺序一致。

内存屏障的作用就是保证在它之前的操作不会被排到它之后,而它之后的操作也不会被排到它之前,从而保证了数据的有序性,避免了由于指令重排导致的数据不一致问题。

2. JVM内存模型(Java Memory Model, JMM)简介

JVM的内存模型,也就是Java内存模型(Java Memory Model,JMM),主要是围绕在并发过程中如何处理原子性、可见性和有序性这三个特性来建立的模型。

在Java中,所有实例域、静态域和数组元素都存储在堆内存中,并且在多个线程之间共享。本地变量、方法定义的参数以及异常处理器参数不会在线程之间共享,它们不会有数据竞争的问题。

线程对这些共享数据的操作必须通过主内存来协调。线程之间不能直接通信,线程对共享数据的所有读写都必须通过主内存进行。但是,为了提高效率,在读取主内存数据时,每个线程会先把变量的值拷贝一份到自己的工作内存(对应于CPU的高速缓存),然后对这些副本进行操作,然后再把新的值写回主内存。

这就可能导致一个问题,两个线程同时操作一个变量时,一个线程修改了这个变量的值,但是这个新值还没有写回主内存时,另一个线程就来读取这个变量的值,这时读到的就是旧的值,也就是说,一个线程对共享变量的修改,另一个线程不一定马上看得到,这就涉及到了可见性问题。

指令重排也可能导致线程看到的变量值不一致。为了提高指令执行的效率,CPU和编译器可能会对指令进行重排。如果没有合适的同步措施,指令重排可能会导致一些非预期的结果,尤其是在多线程环境下。

因此,为了解决可见性和有序性问题,Java内存模型中引入了内存屏障等机制来保证并发时数据的一致性和有序性。

III. Hotspot中的内存屏障

思考几个问题

    1. Hotspot在哪些关键的地方插入了内存屏障
    1. 内存屏障的四种类型:LoadLoad, StoreStore, LoadStore, StoreLoad 分别如何工作
    1. hotspot 如何使用不同类型的内存屏障来实现JMM中的happens-before关系

HotSpot虚拟机作为Java的一种实现,为了保证Java内存模型(Java Memory Model,JMM)中的可见性和有序性,采用了内存屏障(Memory Barrier)的技术。内存屏障是一组特殊的CPU指令,用于防止指令重排和保证数据可见性。HotSpot在关键的地方插入内存屏障,以实现JMM中的happens-before关系。

内存屏障有四种类型:

  1. LoadLoad屏障:确保在LoadLoad屏障之前的所有读操作完成后,才开始执行屏障后面的读操作。

  2. StoreStore屏障:确保在StoreStore屏障之前的所有写操作完成后,才开始执行屏障后面的写操作。

  3. LoadStore屏障:确保在LoadStore屏障之前的所有读操作完成后,才开始执行屏障后面的写操作。

  4. StoreLoad屏障:确保在StoreLoad屏障之前的所有写操作完成后,才开始执行屏障后面的读操作。

不同类型的内存屏障用于实现JMM中的happens-before关系如下:

  1. 对于volatile变量的读操作,需要在读操作前插入LoadLoad屏障,以确保在读取volatile变量之前,其他线程对这个变量的写入已经完成。同时,在读操作后插入LoadStore屏障,确保volatile变量的读取在后续的写操作之前完成。

  2. 对于volatile变量的写操作,需要在写操作前插入StoreStore屏障,确保在写入volatile变量之前,其他线程对这个变量的写入已经完成。同时,在写操作后插入StoreLoad屏障,确保volatile变量的写入在后续的读操作之前完成。

  3. 对于锁的释放操作,需要在释放锁之前插入StoreStore屏障,确保在锁释放之前,对共享数据的写入已经完成。这样,当另一个线程获取到锁时,就能看到前一个线程对共享数据的修改。

  4. 对于锁的获取操作,需要在获取锁之后插入LoadLoad屏障,确保在锁获取之前,对共享数据的读取已经完成。这样,当前线程在获取到锁之后,就能看到之前线程对共享数据的修改。

通过在关键位置插入不同类型的内存屏障,HotSpot虚拟机可以确保在多线程环境下,线程间的数据访问具有一定的顺序性和可见性,从而实现JMM中的happens-before关系。

4. 示例:volatile变量在Hotspot中如何使用内存屏障

  • volatile变量的写操作happens-before其它线程的读操作
  • Hotspot会在volatile变量的读写操作之间插入StoreLoad屏障

volatile变量在Java内存模型中具有特殊的语义,它的写操作happens-before其它线程的读操作。这意味着,一旦一个线程写入了一个volatile变量,随后的其他线程读取这个volatile变量时,能看到这个写操作。

在Hotspot虚拟机中,为了保证volatile变量的语义,会在volatile变量的写操作后和读操作前插入StoreLoad屏障。

当我们对volatile变量进行写操作时,写操作后会插入一个StoreStore屏障,用来防止写操作与后续写操作的重排序,并保证此次写操作对所有处理器可见。然后插入一个StoreLoad屏障,防止写操作与后续读操作的重排序。

当我们对volatile变量进行读操作时,读操作后会插入一个LoadLoad屏障,用来防止读操作与后续读操作的重排序,并保证能读取到最新的数据。同时插入一个LoadStore屏障,防止读操作与后续写操作的重排序。这样就能保证,在读取volatile变量后的所有操作,都能看到volatile变量最新的值。

通过在volatile变量的读写操作之间插入StoreLoad屏障,Hotspot虚拟机能保证volatile变量的写操作happens-before其它线程的读操作,从而实现volatile变量的语义。
假设我们有两个线程,线程A和线程B。线程A负责更新一个volatile变量v,线程B负责读取这个变量。

以下是相关代码:

线程A:

v = 1;  // 写操作
...

线程B:

int a = v;  // 读操作
...

在Hotspot虚拟机中,为了保证volatile的语义,上述代码在实际执行时,会被插入内存屏障,变为如下形式:

线程A:

v = 1;  // 写操作
StoreStore屏障
StoreLoad屏障
...

线程B:

LoadLoad屏障
LoadStore屏障
int a = v;  // 读操作
...

线程A中,v = 1;后会插入StoreStore屏障和StoreLoad屏障。StoreStore屏障用来防止后续的写操作被提前,StoreLoad屏障用来防止后续的读操作被提前。

线程B中,读操作int a = v;前会插入LoadLoad屏障和LoadStore屏障。LoadLoad屏障用来防止后续的读操作被提前,LoadStore屏障用来防止后续的写操作被提前。

通过这种方式,Hotspot虚拟机保证了volatile变量的写操作happens-before其他线程的读操作,即线程B能够看到线程A对变量v的更新。

5. 结论

  • 通过使用内存屏障,Hotspot确保了编译器和处理器在遵循happens-before关系的同时,不会对关键的内存操作进行错误的指令重排
  • 这使得在多线程环境中,程序员可以编写出正确、可靠的代码

6. 参考文档

  1. oracle java JMM 官方文档 : https://docs.oracle.com/javase/specs/jls/se8/html/jls-17.html#jls-17.4

你可能感兴趣的:(JVM从入门到精通,Java并发编程系列,jvm,java,后端)