从Hotsport源码和操作系统级别深入理解volatile关键字与内存屏障(Lock前缀)

一、volatile的内存语义

1.2 volatile的特性

可见性对一个volatile变量的读,总是能看到(任意线程)对这个volatile变量最后的写入。

原子性对任意单个volatile变量的读/写具有原子性,但类似于volatile++这种复合操作不具有原子性(基于这点,我们通过会认为volatile不具备原子性)。volatile仅仅保证对单个volatile变量的读/写具有原子性,而锁的互斥执行的特性可以确保对整个临界区代码的执行具有原子性。

64位的long型和double型变量,只要它是volatile变量,对该变量的读/写就具有原子性。

有序性对volatile修饰的变量的读写操作前后加上各种特定的内存屏障来禁止指令重排序来保障有序性

在JSR-133之前的旧Java内存模型中,虽然不允许volatile变量之间重排序,但旧的Java内存模型允许volatile变量与普通变量重排序。为了提供一种比锁更轻量级的线程之间通信的机制,JSR-133专家组决定增强volatile的内存语义:严格限制编译器和处理器对volatile变量与普通变量的重排序,确保volatile的写-读和锁的释放-获取具有相同的内存语义。

1.2 volatile写-读的内存语义

当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值刷新到主内存。

当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效,线程接下来将从主内存中读取共享变量。

1.3 volatile可见性实现原理

1.3.1 JMM内存交互层面实现

volatile修饰的变量的read、load、use操作和assign、store、write必须是连续的,即修改后必须立即同步回主内存,使用时必须从主内存刷新,由此保证volatile变量操作对多线程的可见性。

1.3.2 汇编层面volatile的实现 (java代码中的使用和验证)

public class VisibilityTest {
    // 方式一:使用volatile
	private volatile boolean flag = true;
.......

上述代码是我们在java中是使用volatile关键字的用法,那么在java中加了这个关键字之后,底层JVM或者操作系统到底是如何处理的呢?为何就能够保证可见性呢?也就是说这个java中的volatile关键字为何能够使得缓存中的值失效,并强迫线程把修改后的值立即刷回主内存的?大家有想过这个问题吗?

其实我们可以查看JIT及时编译后的汇编代码。我们知道java中是存在两种解释器的,一种是字节码解释器,一种是模板解释器。对于热点代码会使用规模解释器直接编译成汇编语言,从而达到更快的执行效率。

我们先下载hsdis-amd64.dll并将其放在 $JAVA_HOME/jre/bin/server 目录下。
在这里插入图片描述

然后给测试程序添加JVM参数:

-XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly

结果:

在这里插入图片描述
可以看到,java代码中加了volatile关键字的变量编译成汇编代码之后,前面是有一个lock; addl $0,0(%%rsp) 的,这也就是我们平时所说的lock前缀

我们知道,Lock前缀是有一种指令,它能够使得缓存将修改后的变量立即刷回主内存,同时使得其他缓存中的该值失效。这就是volatile是如何产生作用的过程。后面我们可以从其他层面更深入的去了解这个关键字。

1.3.3 硬件层面实现

通过lock前缀指令,会锁定变量缓存行区域并写回主内存,这个操作称为“缓存锁定”,缓存一致性机制会阻止同时修改被两个以上处理器缓存的内存区域数据。一个处理器的缓存回写到内存会导致其他处理器的缓存无效。

二、volatile在hotspot的实现

2.1 字节码解释器实现

JVM中的字节码解释器(bytecodeInterpreter),用C++实现了JVM指令,其优点是实现相对简单且容易理解,缺点是执行慢。

我们的java代码使用字节码解释器执行的时候,会首先被解析为C++代码,然后在被解析为汇编、机器码等,最终才能被机器识别。

而在java代码中加了volatile变量的关键字会被虚拟机中bytecodeInterpreter.cpp这个类中的如何方法处理:
在这里插入图片描述
可以看到,这里在判断各种类型的数据(如object,Long, Char, Short…)是否是volatile修饰的。

如果是,最终会调用OrderAccess::storeload() 方法,也就是会插入一个storeload内存屏障

内存屏障的作用和Lock前缀是一样的,都可以使得修改后的数据立即刷回主内存并使得其他缓存中的数据失效(同时也会保证有序性,防止指令重排序!)。在Linux X86架构中,使用Lock前缀来代替内存屏障,因为Lock前缀性能更高。

关于OrderAccess::storeload() 方法的具体实现,请继续往下看,我们会到linux架构代码中去查看盖世仙。

2.2 模板解释器实现

模板解释器(templateInterpreter),其对每个指令都写了一段对应的汇编代码,启动时将每个指令与对应汇编代码入口绑定,可以说是效率做到了极致。

对于一些热点代码,比如while(true) {…}这种要被执行非常多次的代码,为了提高性能、减少字节码解析次数,会使用模板解释器执行,会将该段代码直接编译成一段汇编指令!

templateTable_x86_64.cpp

void TemplateTable::volatile_barrier(Assembler::Membar_mask_bits
                                     order_constraint) {
  // Helper function to insert a is-volatile test and memory barrier
  if (os::is_MP()) { // Not needed on single CPU
    __ membar(order_constraint);
  }
}

// 负责执行putfield或putstatic指令
void TemplateTable::putfield_or_static(int byte_no, bool is_static, RewriteControl rc) {
	// ...
	 // Check for volatile store
    __ testl(rdx, rdx);
    __ jcc(Assembler::zero, notVolatile);

    putfield_or_static_helper(byte_no, is_static, rc, obj, off, flags);
    volatile_barrier(Assembler::Membar_mask_bits(Assembler::StoreLoad |
                                                 Assembler::StoreStore));
    __ jmp(Done);
    __ bind(notVolatile);

    putfield_or_static_helper(byte_no, is_static, rc, obj, off, flags);

    __ bind(Done);
 }

这里会判断处理器是否是多核处理器,如果是则会调用membar方法!

assembler_x86.hpp

// Serializes memory and blows flags
void membar(Membar_mask_bits order_constraint) {
  // We only have to handle StoreLoad
  // x86平台只需要处理StoreLoad
  if (order_constraint & StoreLoad) {

    int offset = -VM_Version::L1_line_size();
    if (offset < -128) {
      offset = -128;
    }

    // 下面这两句插入了一条lock前缀指令: lock addl $0, $0(%rsp) 
    lock(); // lock前缀指令
    addl(Address(rsp, offset), 0); // addl $0, $0(%rsp) 
  }
}

从membar方法中我们也可以看出来,最终volatile的实现是插入了一条Lock前缀指令实现的!!!

三、volatile在linux系统x86中的实现

orderAccess_linux_x86.inline.hpp

inline void OrderAccess::storeload()  { fence(); }
inline void OrderAccess::fence() {
  if (os::is_MP()) {
    // always use locked addl since mfence is sometimes expensive
#ifdef AMD64
    __asm__ volatile ("lock; addl $0,0(%%rsp)" : : : "cc", "memory");
#else
    __asm__ volatile ("lock; addl $0,0(%%esp)" : : : "cc", "memory");
#endif
  }

可以看到,JVM中判断一个变量时volatile修饰的,最终会调用OrderAccess::storeload()方法,即会调用操作系统的库函数。

这里我们可以看到这个函数在linux系统X86中的具体实现,可以看到storeload方法会盗用fence方法,而fence方法中判断如果处理器是多核的,则会使用lock; addl $0,0(%%rsp),也就是Lock前缀来保证可见性和有序性!!!

lock前缀指令的作用

1、确保后续指令执行的原子性。在Pentium及之前的处理器中,带有lock前缀的指令在执行期间会锁住总线,使得其它处理器暂时无法通过总线访问内存,很显然,这个开销很大。在新的处理器中,Intel使用缓存锁定来保证指令执行的原子性,缓存锁定将大大降低lock前缀指令的执行开销

2、LOCK前缀指令具有类似于内存屏障的功能,禁止该指令与前面和后面的读写指令重排序。

3、LOCK前缀指令会等待它之前所有的指令完成、并且所有缓冲的写操作写回内存(也就是将store buffer中的内容写入内存)之后才开始执行,并且根据缓存一致性协议,刷新store buffer的操作会导致其他cache中的副本失效。

四、从硬件层面分析Lock前缀指令

《64-ia-32-architectures-software-developer-vol-3a-part-1-manual.pdf》中有如下描述:

The 32-bit IA-32 processors support locked atomic operations on locations in system memory. These operations are typically used to manage shared data structures (such as semaphores, segment descriptors, system segments, or page tables) in which two or more processors may try simultaneously to modify the same field or flag. The processor uses three interdependent mechanisms for carrying out locked atomic operations:
• Guaranteed atomic operations
• Bus locking, using the LOCK# signal and the LOCK instruction prefix
• Cache coherency protocols that ensure that atomic operations can be carried out on cached data structures (cache lock); this mechanism is present in the Pentium 4, Intel Xeon, and P6 family processors

32位的IA-32处理器支持对系统内存中的位置进行锁定的原子操作。这些操作通常用于管理共享的数据结构(如信号量、段描述符、系统段或页表),在这些结构中,两个或多个处理器可能同时试图修改相同的字段或标志。处理器使用三种相互依赖的机制来执行锁定的原子操作:

  • 有保证的原子操作
  • 总线锁定,使用LOCK#信号和LOCK指令前缀
  • 缓存一致性协议,确保原子操作可以在缓存的数据结构上执行(缓存锁);这种机制出现在Pentium 4、Intel Xeon和P6系列处理器中

五、总结与疑问

至此,我们已经对java中volatile关键字进行了深入的分析,从JVM源码、汇编层面以及操作系统中的具体实现。

我们可以总结到,要实现缓存修改数据后立即刷回主内存、并使得其他缓存中的该数据失效的这种能力,是只有操作操作系统才具备的!!! JVM的实现C++语言不具备、Java语言更不具备,最终都是要调用操作系统级别的库函数,才能够具备这种能力!!!

那问题就到了操作系统这里,操作系统又是如何实现这种功能的呢?我们前面总结说到有添加内存屏障和使用Lock前缀指令的,这又是为什么呢?

因为对于同一功能,不同的操作系统可能会有不同的实现,也许在一些架构中,会使用内存屏障来保证可见性和有序性,但是在一些架构中,比如Linux系统X86中,就是用了与内存屏障相同功能的Lock前缀来实现!因为其认为Lock前缀的性能要优于直接使用内存屏障。

至此,我们已经搞清楚了volatile的整个实现过程与原理。现在再要深入的话,我们可能就会提出这样的问题:

内存屏障具备的能力我们了解了,但是内存屏障真身又是什么呢?在操作系统中又是如何实现的呢?

后面我们持续学习,希望可以解决这个疑问。也欢迎知道答案的人告诉一下。。。@@!

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值