Java volatile禁止指令重排的底层实现(新手必看)
在 Java 中,volatile 关键字确实可以禁止指令重排序,它通过内存屏障来实现这一特性。
若用 volatile 修饰共享变量,JVM 会在读取或写入 volatile 变量时插入特定类型的内存屏障,从而防止指令重排序的发生。
内存屏障是一种处理器指令,它能够影响指令的执行顺序,确保屏障前后的操作执行的有序性,这些内存屏障分为两种:
在 JMM 中,volatile 关键字的一个重要作用是防止指令重排序优化。因为编译器和处理器都可能会对指令进行重排序以优化性能和资源利用率,但这可能会破坏多线程程序的并发性质。
volatile 变量的读写操作将作为一个固定的参考点,编译器和处理器在遇到 volatile 变量时,会按照以下规则进行操作:
这些内存屏障阻止了特定类型的处理器重排序。例如,写 volatile 变量之后的读写操作不会被重排序到写入操作之前。这样,就可以创建 happens-before 关系,确保在 volatile 变量写入之后的操作不会被重排序到其之前,从而保证了线程间的内存可见性和有序性。
因此,volatile 变量的正确使用可以作为一个轻量级的同步机制,从而确保多线程程序中的共享变量访问的可见性和有序性,而无须锁的开销。然而,它并不提供复合操作的原子性(例如递增)。如果需要复杂的同步,通常还是需要使用 synchronized 关键字或 java.util.concurrent 包下的原子类。
值得注意的是,不同的处理器架构可能对内存屏障有不同的实现。例如,在 x86 架构中,读屏障和写屏障由于其较强的内存模型,往往不需要使用显式的屏障指令;而在 ARM 或其他架构中,就需要使用显式的屏障指令来保证 volatile 关键字的语义。
若用 volatile 修饰共享变量,JVM 会在读取或写入 volatile 变量时插入特定类型的内存屏障,从而防止指令重排序的发生。
内存屏障是一种处理器指令,它能够影响指令的执行顺序,确保屏障前后的操作执行的有序性,这些内存屏障分为两种:
- 加载屏障:当读取一个 volatile 变量时,会插入一个加载屏障。这个屏障确保了在读取 volatile 变量之前所有的读写操作都已经完成;
- 存储屏障:当写入一个 volatile 变量时,会插入一个存储屏障。这个屏障确保了之前所有的写入操作都完成,才开始执行写入 volatile 变量的操作。
在 JMM 中,volatile 关键字的一个重要作用是防止指令重排序优化。因为编译器和处理器都可能会对指令进行重排序以优化性能和资源利用率,但这可能会破坏多线程程序的并发性质。
volatile 变量的读写操作将作为一个固定的参考点,编译器和处理器在遇到 volatile 变量时,会按照以下规则进行操作:
- 在每个 volatile 写入操作的前面插入一个存储屏障;
- 在每个 volatile 写入操作的后面插入一个 StoreLoad 屏障;
- 在每个 volatile 读取操作的后面插入一个加载屏障和 LoadStore 屏障。
这些内存屏障阻止了特定类型的处理器重排序。例如,写 volatile 变量之后的读写操作不会被重排序到写入操作之前。这样,就可以创建 happens-before 关系,确保在 volatile 变量写入之后的操作不会被重排序到其之前,从而保证了线程间的内存可见性和有序性。
因此,volatile 变量的正确使用可以作为一个轻量级的同步机制,从而确保多线程程序中的共享变量访问的可见性和有序性,而无须锁的开销。然而,它并不提供复合操作的原子性(例如递增)。如果需要复杂的同步,通常还是需要使用 synchronized 关键字或 java.util.concurrent 包下的原子类。
值得注意的是,不同的处理器架构可能对内存屏障有不同的实现。例如,在 x86 架构中,读屏障和写屏障由于其较强的内存模型,往往不需要使用显式的屏障指令;而在 ARM 或其他架构中,就需要使用显式的屏障指令来保证 volatile 关键字的语义。