Java 8不安全:xxxFence()指令

在Java 8中,三个内存屏障指令被添加到Unsafe类( 源 )中:

 /** * Ensures lack of reordering of loads before the fence * with loads or stores after the fence. */ void loadFence(); /** * Ensures lack of reordering of stores before the fence * with loads or stores after the fence. */ void storeFence(); /** * Ensures lack of reordering of loads or stores before the fence * with loads or stores after the fence. */ void fullFence(); 

如果我们用下面的方式来定义记忆障碍(我认为这或多或less容易理解):

考虑X和Y是需要重新sorting的操作types/类,

X_YFence()是一个内存屏障指令,用于确保屏障启动之前,typesY的任何操作之前完成屏障之前的typesX的所有操作。

我们现在可以将屏障名称从“ Unsafe ”映射到这个术语:

  • loadFence()变成load_loadstoreFence() ;
  • storeFence()变成storeFence() store_loadStoreFence() ;
  • fullFence()变成loadstore_loadstoreFence() ;

最后, 我的问题是 – 为什么我们没有load_storeFence() store_loadFence()store_storeFence()store_storeFence()load_loadFence()

我的猜测是 – 他们并不是真的必要的,但我现在不明白为什么。 所以,我想知道不添加它们的原因。 对此的猜测也是受欢迎的(希望这不会导致这个问题偏离基于意见的观点)。

提前致谢。

概要

CPU内核具有特殊的内存sorting缓冲区,以帮助他们乱序执行。 这些可以(通常是)加载和存储分开:加载顺序缓冲区的LOB和商店顺序缓冲区的SOB。

针对不安全APIselect的屏蔽操作是基于以下假设来select的:底层处理器将具有单独的加载顺序缓冲区(用于重新sorting加载),商店顺序缓冲区(用于重新sorting商店)。

因此,基于这个假设,从软件的angular度来看,你可以从CPU中请求三件事之一:

  1. 清空LOB(loadFence):意味着在该内核上没有其他指令将开始执行,直到所有的LOB被处理。 在x86中,这是一个LFENCE。
  2. 清空SOB(storeFence):意味着在这个核心中没有其他的指令会开始执行,直到SOB中的所有条目都被处理完毕。 在x86中,这是一个SFENCE。
  3. 清空LOB和SOB(fullFence):意味着上述两者。 在x86中这是一个MFENCE。

实际上,每个特定的处理器架构提供了不同的存储器次序保证,这可能比上述更严格,或者更灵活。 例如,SPARC体系结构可以重新sorting加载存储和存储加载序列,而x86不会这样做。 此外,存在LOB和SOB不能单独控制的架构(即,只有全围栏是可能的)。 但是在这两种情况下:

  • 当架构更灵活时,API根本不提供对“放大器”sorting组合的访问权限

  • 当架构更为严格时,API在所有情况下都会执行更严格的顺序保证(例如,实际上所有3个调用都是作为完整的栅栏实现的)

在JEP中根据答案assylias提供的具体APIselect的原因在现场100%解释。 如果你知道内存sorting和caching一致性,assylias的答案应该足够了。 我认为,它们与C ++ API中的标准化指令相匹配是一个主要因素(简化了JVM实现): http : //en.cppreference.com/w/cpp/atomic/memory_order很可能,实际实现将调用相应的C ++ API而不是使用一些特殊的指令。

下面我对基于x86的示例有详细的解释,它将提供理解这些内容所需的所有上下文。 实际上,划定的区域(下面的部分回答了另一个问题:“您能否提供内存隔离如何在x86架构中控制caching一致性的基本示例?

原因是我自己(来自软件开发人员而不是硬件devise人员)很难理解什么是内存重新sorting,直到我学到了如何实现caching一致性在x86中的具体例子。 这为讨论内存围栏提供了非常宝贵的背景(对于其他体系结构)。 最后,我使用从x86例子中获得的知识来讨论SPARC

参考文献[1]是一个更详细的解释,并且有单独讨论x86,SPARC,ARM和PowerPC的部分,所以如果您对更多细节感兴趣,那么这是一个很好的解读。


x86架构示例

x86提供了3种types的屏蔽指令:LFENCE(load fence),SFENCE(store fence)和MFENCE(load-store fence),所以它将100%映射到Java API。

这是因为x86具有独立的加载顺序缓冲区(LOB)和存储顺序缓冲区(SOB),所以LFENCE / SFENCE指令确实适用于各自的缓冲区,而MFENCE则适用于两者。

SOB用于存储输出值(从处理器到高速caching系统),而高速caching一致性协议则用于获取写入高速caching行的权限。 LOB被用来存储失效请求,这样失效可以asynchronous执行(减less了接收端的延迟,希望在那里执行的代码实际上不需要这个值)。

无序商店和SFENCE

假设你有一个双处理器系统和两个CPU,0和1,执行下面的例程。 考虑高速caching线路保持failure最初由CPU1拥有的情况,而保持shutdown的高速caching线路最初由CPU0拥有。

 // CPU 0: void shutDownWithFailure(void) { failure = 1; // must use SOB as this is owned by CPU 1 shutdown = 1; // can execute immediately as it is owned be CPU 0 } // CPU1: void workLoop(void) { while (shutdown == 0) { ... } if (failure) { ...} } 

在没有存储围栏的情况下,CPU 0可能由于故障而发出closures信号,但是CPU 1将退出该循环并且不进入故障处理(如果阻塞)。

这是因为CPU0将写入failure的值1给存储顺序缓冲区,同时发出caching一致性消息来获得对caching线的独占访问。 然后进入下一条指令(等待独占访问)并立即更新shutdown标志(这个高速caching线由CPU0专有,因此不需要与其他内核协商)。 最后,当它稍后收到来自CPU1的失效确认消息(关于failure )时,它将继续处理SOB failure并将该值写入到caching(但顺序现在颠倒过来)。

插入一个storeFence()将修复一些事情:

 // CPU 0: void shutDownWithFailure(void) { failure = 1; // must use SOB as this is owned by CPU 1 SFENCE // next instruction will execute after all SOBs are processed shutdown = 1; // can execute immediately as it is owned be CPU 0 } // CPU1: void workLoop(void) { while (shutdown == 0) { ... } if (failure) { ...} } 

值得一提的最后一个方面是x86有存储转发(memory-forwarding):当一个CPU写入一个被locking在SOB中的值(由于caching一致性)时,它可能会在SOB之前尝试执行一个相同地址的加载指令处理并传递到caching。 因此,CPU将查阅SOB的PRIOR来访问caching,所以在这种情况下检索的值是来自SOB的最后写入的值。 这意味着无论如何,这个核心的商店永远不会被这个核心的后续加载重新sorting

无序加载和LFENCE

现在,假设你已经拥有了商店的围墙,并且很高兴在CPU 1的途中shutdown不能超越failure ,而把重点放在另一边。 即使在商店围墙的存在下,也存在发生错误的情况。 考虑在两个高速caching(共享)中都failure的情况,而shutdown仅在CPU0的高速caching中存在并仅由其拥有。 坏事可能发生如下:

  1. CPU0写入1 failure ; 它还向CPU1发送消息,使其共享caching行的副本无效,作为caching一致性协议的一部分
  2. CPU0执行SFENCE并停止,等待用于failure的SOB提交。
  3. CPU1检查由于while循环引起的shutdown ,并且(实现它缺less值)发送caching一致性消息来读取值。
  4. CPU1在步骤1中接收来自CPU0的消息以使失效failure ,并立即发送确认。 注意:这是使用无效队列实现的,所以实际上它只是input一个注释(在它的LOB中分配一个条目),稍后进行无效操作,但是在发送确认之前并不实际执行它。
  5. CPU0收到failure确认并继续经过SFENCE到下一条指令
  6. CPU0写1closures而不使用SOB,因为它已经独占了caching行。 由于高速caching行对CPU0是独占的,因此不会发送用于无效的额外消息
  7. CPU1收到shutdown值并将其提交到本地caching,然后进入下一行。
  8. CPU1检查if语句的failure值,但由于无效队列(LOB注释)尚未处理,因此它使用本地caching中的值0(不inputif block)。
  9. CPU1处理无效队列并将failure更新为1,但已经太晚了…

我们称之为加载顺序缓冲区,实际上是无效请求的排队,上面的问题可以用下面的方法来解决:

 // CPU 0: void shutDownWithFailure(void) { failure = 1; // must use SOB as this is owned by CPU 1 SFENCE // next instruction will execute after all SOBs are processed shutdown = 1; // can execute immediately as it is owned be CPU 0 } // CPU1: void workLoop(void) { while (shutdown == 0) { ... } LFENCE // next instruction will execute after all LOBs are processed if (failure) { ...} } 

你在x86上的问题

现在你知道SOB / LOB做什么了,考虑一下你提到的组合:

 loadFence() becomes load_loadstoreFence(); 

不,加载围栏等待LOB被处理,实质上清空无效队列。 这意味着所有后续的加载将会看到最新的数据(不重新sorting),因为它们将从caching子系统(这是连贯的)中获取。 商店CANNNOT可以随后加载,因为它们不通过LOB。 (并且进一步存储转发照顾本地修改的cachce行)从该特定核心(执行加载围栏的那个)的angular度来看,在加载围栏之后的存储将在所有的寄存器具有加载的数据之后执行。 没有其他办法了。

 load_storeFence() becomes ??? 

没有必要使用load_storeFence,因为它没有任何意义。 要存储的东西,你必须使用input来计算它。 要获取input,您必须执行加载。 商店将使用从负载提取的数据进行。 如果您想确保在加载时使用loadFence,可以看到来自所有OTHER处理器的最新值。 对于篱笆存储转发后的负载需要保持一致的顺序。

所有其他情况是相似的。


SPARC

SPARC更加灵活,可以随后重新安排商店(以及随后的商店)。 我对SPARC并不熟悉,所以我的GUESS是没有存储转发(在重新加载地址时没有查阅SOB),所以“脏读”是可能的。 事实上,我错了:我在[3]中find了SPARC体系结构,现实情况是存储转发是线程化的。 从第5.3.4节:

所有负载检查存储缓冲区(仅相同的线程)以便在写入(RAW)危险之后进行读取。 当加载的DWORD地址与STB中的存储的DWORD地址相匹配,并且加载的所有字节在存储缓冲区中有效时,就会发生完整的RAW。 dword地址匹配时会发生部分RAW,但所有字节在存储缓冲区中都是无效的。 (例如,由于完整的双字不在存储缓冲区条目中,ST(字存储)后跟LDX(双字加载)到相同的地址导致部分RAW)。

所以,不同的线程会查询不同的存储顺序缓冲区,因此存储后可能会发生脏读。


参考

内存障碍:软件黑客的硬件视图,Linux技术中心,IBM Beaverton

[2]英特尔®64和IA-32架构软件开发人员手册,卷3A http://www.intel.com/content/dam/www/public/us/en/documents/manuals/64-ia-32-architectures-软件开发者-VOL-3A-部分-1-手册;.pdf

[3] OpenSPARC T2核心微体系结构规范http://www.oracle.com/technetwork/systems/opensparc/t2-06-opensparct2-core-microarch-1537749.html

JEP 171本身就是一个很好的信息来源。

理由:

这三种方法提供了一些编译器和处理器需要的三种不同types的内存隔离,以确保特定的访问(加载和存储)不会被重新sorting。

实施(摘录):

对于C ++运行时版本(在prims / unsafe.cpp中),通过现有的OrderAccess方法实现:

  loadFence: { OrderAccess::acquire(); } storeFence: { OrderAccess::release(); } fullFence: { OrderAccess::fence(); } 

换句话说,新的方法与在JVM和CPU级别如何实现内存隔离密切相关。 它们也符合C ++中可用的内存屏障指令,C ++是实现热点的语言。

细粒度的方法可能是可行的,但好处并不明显。

例如,如果您查看JSR 133 Cookbook中的cpu指令表,您将看到LoadStore和LoadLoad在大多数体系结构上都映射到相同的指令,即都是有效的Load_LoadStore指令。 所以在JVM级别有一个Load_LoadStore( loadFence )指令看起来是一个合理的devise决策。

storeFence()的doc是错误的。 请参阅https://bugs.openjdk.java.net/browse/JDK-8038978

loadFence()是LoadLoad加上LoadStore,所以经常被称为acquire fence。

storeFence()是StoreStore加LoadStore,所以经常被称为释放篱笆。

LoadLoad LoadStore StoreStore是便宜的防护(x86或Sparc上的nop,Power上的便宜,ARM上的昂贵)。

IA64有不同的获取和释放语义的指令。

fullFence()是LoadLoad LoadStore StoreStore加StoreLoad。

StordLoad栅栏价格昂贵(几乎所有的CPU),几乎与全栅栏一样昂贵。

这certificate了API的devise。