2024-03-23  阅读(49)
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。 本文链接:https://www.skjava.com/mianshi/baodian/detail/1489840329

回答

指令重排序是指在执行程序时,为了提高性能,处理器可能会改变指令的执行顺序。

volatile 通过内存屏障来防止指令重排序,保证有序性。volatile 提供了两种内存屏障:

  • 写内存屏障:写内存屏障设置在写 volatile 变量之后。它确保对该 volatile 变量的所有写操作在任何后续对同一变量的读操作之前完成。这就意味着,在写内存屏障之前的所有普通写操作(不仅仅是对 volatile 变量的写操作)都将在写入 volatile 变量之前完成。
  • 读内存屏障:读内存屏障设置在读 volatile 变量之前。它确保对该 volatile 变量的所有读操作在任何先前的写操作之后完成。这就意味着,所有在读内存屏障之后的普通读操作(不仅仅是对 volatile 变量的读操作)都将在读取 volatile 变量之后进行。

这种内存屏障阻止了指令重排序,确保在 volatile 变量之前的操作不会被重排序到其之后,同时也确保在 volatile 变量之后的操作不会被重排序到其之前。

详细分析

指令重排序

为了提高应用程序的性能,编译器和处理器都会我们的指令按照某种规则重新排序。简单来说就是指我们在程序中写的代码,在执行时并不一定按照写的顺序执行**。**比如下面这段代码:

int i = 0;                       // 1     
boolean flag = false;            // 2
String str = "死磕 Java";         // 3

按照我们常规的思路,程序执行的顺序就是 1 —> 2 —> 3,但是在程序运行时,执行顺序就无法确认了,可能是 1 —> 3 —> 2 ,也有可能是 3 —> 2 —> 1等等。

但是无论如何指令重排序,它都不能改变程序最终的运行结果,例如:

int i = 1;                // 1
int k = i + 3;            // 2
int m = i + 2 * k;        // 3

这种情况下能否进行指令重排序?显然不能,如果你改变他们的执行顺序会导致程序运行结果的变化。所以指令重排序要遵循如下几个规则:

  1. 程序顺序规则:对于单线程来说,重排序后的程序执行结果应该与该程序代码顺序执行的结果一致。也就是在单线程环境下不能改变程序运行的结果。
  2. 数据依赖性规则:重排序过程中必须保持数据之间的依赖性。

指令重排序有 3 种

  • 编译器重排序:编译器在不改变单线程程序的语义前提下,可以重新安排语句的执行顺序。它是源代码级别。
  • 处理器重排序:处理器可能会基于其内部逻辑,如指令流水线、执行单元的可用性等因素在执行时对指令进行重排序。
  • 内存重排序:由于目前都是多核处理器和多级缓存,内存操作的执行顺序可能会与程序中的顺序不同。

as-if-serial 语义

as-if-serial语义是:所有的操作均可以为了优化而被重排序,但是你必须要保证重排序后执行的结果不能被改变。上面所提到的程序顺序规则数据依赖性规则就是 as-if-serial的语义。简单举个例子:

int a = 1 ;       // A
int b = 2 ;       // B
int c = a + b;    // C

A、B、C三个操作存在如下关系:A、B不存在数据依赖关系,A 和C、B 和 C 存在数据依赖关系,因此在进行重排序的时候,A、B可以随意排序,但是必须位于 C 的前面,执行顺序可以是A —> B —> C或者B —> A —> C。但是无论是何种执行顺序最终的结果 C 总是等于3。 as-if-serail语义把单线程程序保护起来了,它可以保证在重排序的前提下程序的最终结果始终都是一致的。

as-if-serail 保证的是单线程环境下的,那多线程呢?这就要依赖 happens-before 原则了。

happens-before

happens-before 定义了在多线程环境中,内存的写操作和读操作之间的顺序关系。它确保了多线程环境下,线程间共享变量的操作是有序且可见的。即:

在 Java 内存模型中,如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须存在happens-before关系。

happens-before 原则有如下八大原则:

  1. 程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作。
  2. 锁定规则:一个 unLock 操作先行发生于后面对同一个锁额 lock 操作。
  3. volatile变量规则:对一个 volatile 变量的写操作先行发生于后面对这个变量的读操作。
  4. 传递规则:如果操作 A 先行发生于操作 B,而操作 B 又先行发生于操作 C,则可以得出操作 A 先行发生于操作 C。
  5. 线程启动规则:Thread 对象的start() 方法先行发生于此线程的每个一个动作。
  6. 线程中断规则:对线程interrupt() 方法的调用先行发生于被中断线程的代码检测到中断事件的发生。
  7. 线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过 Thread.join() 方法结束、Thread.isAlive() 的返回值手段检测到线程已经终止执行。
  8. 对象终结规则:一个对象的初始化完成先行发生于他的 finalize() 方法的开始。

如果两个操作不满足上述八大原则中的任意一个,那么这两个操作就没有顺序保证,虚拟机可以对这两个操作进行重排序。如果操作 A happens-before 操作 B,那么 A 在内存所做的修改对 B 都是可见的。

关于 happens-before 原则,大明哥这篇文章有非常详细的介绍:

https://www.skjava.com/series/article/1711591604

volatile 使用内存屏障保证有序性

什么是内存屏障

什么是内存屏障?内存屏障其实就是一个 CPU 指令,我们可以理解它是一种指令级别的同步机制,它用于确保指令的执行顺序以及内存操作的可见性和顺序性。

从硬件层面来说,内存屏障分为两种:

  • Load Barriers:加载屏障,确保所有在 Load Barriers 屏障之前的读操作完成后,才执行屏障之后的读操作。
  • Store Barriers:存储屏障,确保所有在 Store Barriers** **屏障之前的写操作完成后,才执行屏障之后的写操作。

从 JVM 层面来说,内存屏障分为四种:

  • LoadLoad
    • 确保 LoadLoad 屏障之前的所有加载操作(Load)在内存中完成后,才能执行屏障之后的加载操作。
    • 例如:Load1; LoadLoad; Load2,保证 load1 的读操作先于 load2 执行。
  • StoreStore
    • 确保 StoreStore 屏障之前的所有存储操作(Store)在内存中完成后,才能执行屏障之后的存储操作。
    • 例如:Store1; StoreStore; Store2,保证 store1 的写操作先于 store2 执行,并刷新到主内存。
  • LoadStore
    • 确保 LoadStore 屏障之前的加载操作在内存中完成后,才能执行屏障之后的存储操作。
    • 例如:Load1; LoadStore; Store2,保证 load1 的读操作结束先于 load2 的写操作执行。
  • StoreLoad
    • 最强内存屏障,确保 StoreLoad 屏障之前的所有存储操作完成后,才能执行屏障之后的加载操作。
    • 例如:Store1; StoreLoad; Load2,保证 store1 的写操作已刷新到主内存之后,load2 及其后的读操作才能执行。

volatile 是如何使用内存屏障保证有序性的?

Volatile 通过内存屏障可以禁止指令重排序,保证了操作的有序性,其规则如下:

  1. 如果第一个操作为volatile读,则不管第二个操作是啥,都不能重排序。这个操作确保volatile读之后的操作不会被编译器重排序到volatile读之前;
  2. 当第二个操作为volatile写是,则不管第一个操作是啥,都不能重排序。这个操作确保volatile写之前的操作不会被编译器重排序到volatile写之后;
  3. 当第一个操作volatile写,第二操作为volatile读时,不能重排序。

图例如下:

那是插入什么内存屏障来保持的呢?

  • 在每一个 volatile 写操作前面插入一个 StoreStore 屏障,禁止上面的普通写与 volatile 写重排序。
  • 在每一个 volatile 写操作后面插入一个 StoreLoad 屏障,禁止 volatile 写于后面的可能存在的 volatile 读/写重排序
  • 在每一个 volatile 读操作后面插入一个 LoadLoad 屏障,禁止 volatile 读与下面的普通读重排序。
  • 在每一个 volatile 读操作后面插入一个 LoadStore 屏障,禁止 volatile 读与下面的普通写重排序。

下面用一个例子来说明下:

public class VolatileTest {
    int i = 0;
    volatile boolean flag = false;
    public void write(){
        i = 2;
        flag = true;
    }

    public void read(){
        if(flag){
            System.out.println("---i = " + i);
        }
    }
}

内存屏障如下:

阅读全文
  • 点赞