前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >阿里面试官:说说你对java虚拟机中,并发设施和指令重排序的理解!

阿里面试官:说说你对java虚拟机中,并发设施和指令重排序的理解!

原创
作者头像
Java程序猿
修改2021-03-15 10:07:22
5860
修改2021-03-15 10:07:22
举报
文章被收录于专栏:Java核心技术Java核心技术

前言

对于一名高级 Java 工程师来说,JVM 可以说是面试必问的一个知识点,而大多数人可能没有对 JVM 的实际开发和使用经验,接下来这一系列文章将带你深入了解 JVM 需要掌握的各个知识点。这也将帮助你完成从初级程序员到高级程序员的转变。关于Java内存模型整理了一份+笔记,地址:Java后端面试真题

并发设施

并发是Java的一大特色,通过并发,可以在Java层实现多个线程协同工作或者互斥执行。上层应用的易用性、安全性、高效性都是由HotSpot VM中的并发设施来保证的。并发设施是HotSpot VM中相当复杂的组件,本章将简单讨论虚拟机在并发方面付出的努力。

指令重排序

开发者专注于代码层面,他们使用高级语言表达自己的思想,使用控制流控制程序执行路径,他们编写的代码会被编译器翻译为底层硬件能理解的低级指令并交由CPU执行。这个过程涉及的硬件系统包括编译器、CPU、Cache等,这些系统中的成员都想尽力把事情做好:编译器可能进行指令调度,可能消除内存访问;CPU为了流水线饱,可能乱序执行指令,可能执行分支预测;Cache可以预取指令或者存储一些程序的执行状态。所有系统组合到一起的效果是程序顺序(代码顺序)与硬件执行指令的执行顺序大相径庭,这个现象即指令重排序。指令重排序会导致多线程环境下程序的行为与开发者预期的不一样,甚至出现严重问题。本节将简单讨论指令重排序出现的原因,并给出对应的硬件解决方案。

编译器重排序

CPU执行寄存器读写的速度比主存读写快一个或多个数量级。读写操作如果命中L1、L2缓存,那么比从主存中读写快,比从寄存器中读写慢。现代处理器通常使用流水线将不同指令的不同部分放到一起执行,而指令重排序正是为了避免因流水线造成的操作等待。

指令重排序有且只有一条规则,即指令重排序不会改变单线程程序的语意,除此之外没有任何限制。如果编译器发现将一个写操作放到读操作后面可能会提升性能,同时这样做不会改变单线程程序的语意,那么编译器就会对代码进行重排序,如代码清单6-1所示:代码清单6-1 编译器重排序(C++

int v1, v2;void foo(){v1 = v2 + 1;v2 = 0;}

代码中v1位于v2前面,使用gcc 9.2 -O3编译后可得到如代码清单6-2所示的指令:

代码清单6-2 编译器重排序(汇编)

foo:mov eax, DWORD PTR v2[rip]mov DWORD PTR v2[rip], 0add eax, 1mov DWORD PTR v1[rip], eaxret

在编译后的代码中,v2先于v1赋值。如果是多线程程序,开发者认为代码顺序就是执行顺序,即v1先于v2执行,就可能产生错误。对于编译器重排序,可以使用编译器提供的编译器屏障(Compiler Barrier)阻止,如GCC使用代码清单6-3所示的编译器屏障阻止重排序:代码清单6-3 编译器屏障

__asm__ volatile ("" : : : "memory");

代码清单6-4演示了如何在v1与v2之间插入编译器屏障解决编译器重排序的问题:

代码清单6-4 插入编译器屏障(C++

int v1, v2;void foo(){v1 = v2 + 1;__asm__ volatile ("" : : : "memory");v2 = 0;}

再次编译后得到如代码清单6-5所示的汇编代码:

代码清单6-5 插入编译器屏障(汇编)

foo:mov eax, DWORD PTR v2[rip]add eax, 1mov DWORD PTR v1[rip], eaxmov DWORD PTR v2[rip], 0ret

在编译后的代码中,v2先于v1赋值,代码没有被编译器重排序,编译器屏障被证明为有效。

处理器重排序

编译器屏障解决了编译器重排序问题,但是并不能完全解决问题,即使消除了编译器重排序,CPU也可能对指令进行重排序,出现类似编译器重排序后的代码序列。CPU级的指令重排序又与CPU架构相关,具体如图6-1所示。

如果把指令抽象为读和写两类,那么两者组合后共有四种重排序规则。注意,x86只允许一种重排序规则,即Store操作被重排序到Load后面,而原来的StoreLoad操作变成LoadStore操作,对于CPU级别的指令重排序,我们需要同样由CPU指令集提供的内存屏障(MemoryBarrier)指令来阻止。在HotSpot VM中,指令内存屏障的实现位于OrderAccess模块,以x86为例,它的各种内存屏障实现如代码清单6-6所示:

代码清单6-6 x86OrderAccess

static inline void compiler_barrier() {__asm__ volatile ("" : : : "memory");}inline void OrderAccess::loadload() { compiler_barrier(); }inline void OrderAccess::storestore() { compiler_barrier(); }inline void OrderAccess::loadstore() { compiler_barrier(); }inline void OrderAccess::storeload() { fence(); }inline void OrderAccess::fence() {#ifdef AMD64__asm__ volatile ("lock; addl $0,0(%%rsp)" : : : "cc", "memory");#else__asm__ volatile ("lock; addl $0,0(%%esp)" : : : "cc", "memory");#endifcompiler_barrier();}

上面的代码是GCC的扩展内联汇编形式,这里的关键字volatile表示禁止编译器优化汇编代码。memory告知编译器汇编代码执行内存读取和写入操作,编译器可能需要在执行汇编前将一些指定的寄存器刷入内存。

由于x86只支持StoreLoad重排序,所以x86上的OrderAccess只实现了storeload(),对于其他重排序类型,可以使用编译器屏障简单代替。

虽然x86指令集有专门的内存屏障指令,如lfence、sfence、mfence,但是OrderAccess::storeload()使用了指令加上lock前缀来当作内存屏障指令,因为lock指令前缀具有内存屏障的语意且有时候比mfence等指令的开销小。

除了LoadLoad、LoadStore、StoreStore、StoreLoad这四种基本内存屏障外,HotSpot VM还定义了特殊的acquire和release内存屏障:acquire防止它后面的读写操作重排序到acquire的前面;release防止它前面的读写操作重排序到release后面。acqure和release两者放在一起就像一个“栅栏”,可禁止“栅栏”内的事务跑到“栅栏”外,但是它不阻止“栅栏”外的事务跑到“栅栏”内部。之所以说acquire和release特殊是因为它们两个可以通过基本内存屏障组合而成:acquire可由LoadLoad和LoadStore组合而成,release可由StoreStore和LoadStore组合而成。另一个值得注意的地方是acquire和release都没有使用StoreLoad屏障,这意味着x86架构原生就具有acquire和release语意。

在Java层面操作内存屏障的办法是Unsafe.loadFence()、Unsafe.storeFence()和Unsafe.fullFence(),它们分别对应OrderAccess::acquire()、OrderAccess::release()、OrderAccess::fence()1。

注意,四种基本内存屏障是无法在Java层直接使用的。如何放置内存屏障是极具挑战的,它们通常出现在高级并发编程中,是专家级并发开发者的任务,在大多数情况下缺少它们不会产生影响,但是在高并发场景下缺少它们通常是致命的。HotSpot VM内部使用了大量的内存屏障,如代码清单6-7所示:

代码清单6-7 OrderAccess的使用

void Method::set_code(...) {...OrderAccess::storestore();mh->_from_compiled_entry = code->verified_entry_point();OrderAccess::storestore();if (!mh->is_method_handle_intrinsic())mh->_from_interpreted_entry = mh->get_i2c_entry();}

由于解释器会从_from_interpretered_entry跳转到_from_compiled_entry,所以在_from_interpretered_entry设置好后必须保证_from_compiled_entry可用,如果没有内存屏障,CPU可能会将_from_compiled_entry的设置重排序到_from_interpretered_entry后面导致错误,所以需要OrderAccess::storestore指明禁止弱内存模型的StoreStore指令重排序。借助这些内存屏障,现在我们可以开始定义一个语义良好、可预测的内存模型。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • 并发设施
  • 指令重排序
  • 编译器重排序
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档