实施从 Unsafe.putOrdered*() 发布的获取?
Implementing an acquire for a release from Unsafe.putOrdered*()?
您认为在 Java 中实现 release/acquire 对的获取部分的最佳正确方法是什么?
我正在尝试使用经典 release/acquire 语义(没有 StoreLoad
并且没有跨线程的顺序一致性)对我的应用程序中的一些操作进行建模。
有几种方法可以在 JDK 中实现大致相当于商店发布的效果。 java.util.concurrent.Atomic*.lazySet()
和基础 sun.misc.Unsafe.putOrdered*()
是最常被引用的方法。然而,没有明显的方法来实现加载获取。
允许lazySet()
的JDKAPI主要在内部使用volatile
变量,因此它们的存储发布与易变负载配对。从理论上讲,易失性加载应该比加载获取更昂贵,并且在之前的存储发布的上下文中不应提供比纯加载获取更多的东西。
sun.misc.Unsafe
不提供 getAcquire()*
等同于 putOrdered*()
方法,即使此类获取方法已计划用于即将推出的 VarHandles API。
听起来可行的是简单加载,然后是 sun.misc.Unsafe.loadFence()
。我在其他任何地方都没有看到这一点,这有点令人不安。这可能与它是一个非常丑陋的 hack 的事实有关。
P.S。我很清楚 JMM 没有涵盖这些机制,它们不足以维持顺序一致性,并且它们创建的操作不是同步操作(例如,我知道它们会破坏 IRIW)。我还了解到,Atomic*/Unsafe
提供的商店发布最常用于急切清空引用或在 producer/consumer 场景中,作为某些重要索引的优化消息传递机制。
根据您的具体要求,在 Java 中最好执行非易失性加载,然后可能是易失性加载。
您可以结合使用
int permits = theUnsafe.getInt(object, offset);
if (!enough(permits))
permits = theUnsafe.getVolatileInt(object, offset);
此模式可用于环形缓冲区,以最大限度地减少缓存行的流失。
volatile read 正是您要找的。
其实对应的volatile操作已经有了release/acquire语义(否则happens-before不可能成对的volatile write-read),但是成对的volatile操作不应该只是顺序一致(~happens-before ),但它们也应该在 total synchronization order 中,这就是为什么 StoreLoad
barrier 在 volatile write 之后插入:以保证 volatile 写入不同位置的总顺序,因此所有线程将以相同的顺序看到这些值.
易失性读取具有获取语义:proof from hotspot codebase, also there is direct recommendation by Doug Lea in JSR-133 cookbook(每次易失性读取后的 LoadLoad
和 LoadStore
障碍)。
Unsafe.loadFence()
也有 acquire 语义(proof), but used not to read value (you can do the same with plain volatile read), but to prevent reorder plain reads with subsequent volatile read. This is used in StampedLock 用于乐观阅读(参见 StampedLock#validate
方法实现和用法)。
在评论中讨论后更新。
让我们检查一下 Unsafe#loadStore()
和 volatile read 是否相同并且具有 acquire 语义。
我正在查看热点 C1 compiler source code 以避免通读 C2 中的所有优化。
它将字节码(实际上,不是字节码,而是它的解释器表示)转换为 LIR(低级中间表示),然后根据目标微体系结构将图形转换为实际操作码。
Unsafe#loadFence
是 intrinsic which has _loadFence
alias. In C1 LIR generator 它生成这个:
case vmIntrinsics::_loadFence :
if (os::is_MP()) __ membar_acquire();
其中 __
是用于生成 LIR 的宏。
现在让我们看看同一 LIR 生成器中的易失性读取 implementation。它尝试插入空值检查,检查 IRIW,检查我们是否在 x32 上并尝试读取 64 位值(用 SSE/FPU 创造一些魔法),最后,将我们引向相同的代码:
if (is_volatile && os::is_MP()) {
__ membar_acquire();
}
汇编程序生成器然后插入特定于平台的获取指令here。
看具体实现(这里没有链接,都可以在src/cpu/{$cpu_model}/vm/c1_LIRAssembler_{$cpu_model}.cpp中找到)
SPARC
void LIR_Assembler::membar_acquire() {
// no-op on TSO
}
x86
void LIR_Assembler::membar_acquire() {
// No x86 machines currently require load fences
}
Aarch64(弱内存模型,应该存在障碍)
void LIR_Assembler::membar_acquire() {
__ membar(Assembler::LoadLoad|Assembler::LoadStore);
}
根据 aarch architecture description 这样的 membar 将在加载后编译为 dmb ishld
指令。
PowerPC(也是弱内存模型)
void LIR_Assembler::membar_acquire() {
__ acquire();
}
然后转换为特定的 PowerPC 指令 lwsync
。根据 comments lwsync
在语义上等同于
lwsync orders Store|Store,
Load|Store,
Load|Load,
but not Store|Load
但只要 PowerPC 没有任何较弱的障碍,这是在 PowerPC 上实现获取语义的唯一选择。
结论
易失性读取和 Unsafe#loadFence()
在内存排序方面相同(但在可能的编译器优化方面可能不同),在最流行的 x86 上它是无操作的,PowerPC 是唯一支持的架构没有确切的获取障碍。
您认为在 Java 中实现 release/acquire 对的获取部分的最佳正确方法是什么?
我正在尝试使用经典 release/acquire 语义(没有 StoreLoad
并且没有跨线程的顺序一致性)对我的应用程序中的一些操作进行建模。
有几种方法可以在 JDK 中实现大致相当于商店发布的效果。 java.util.concurrent.Atomic*.lazySet()
和基础 sun.misc.Unsafe.putOrdered*()
是最常被引用的方法。然而,没有明显的方法来实现加载获取。
允许
lazySet()
的JDKAPI主要在内部使用volatile
变量,因此它们的存储发布与易变负载配对。从理论上讲,易失性加载应该比加载获取更昂贵,并且在之前的存储发布的上下文中不应提供比纯加载获取更多的东西。sun.misc.Unsafe
不提供getAcquire()*
等同于putOrdered*()
方法,即使此类获取方法已计划用于即将推出的 VarHandles API。听起来可行的是简单加载,然后是
sun.misc.Unsafe.loadFence()
。我在其他任何地方都没有看到这一点,这有点令人不安。这可能与它是一个非常丑陋的 hack 的事实有关。
P.S。我很清楚 JMM 没有涵盖这些机制,它们不足以维持顺序一致性,并且它们创建的操作不是同步操作(例如,我知道它们会破坏 IRIW)。我还了解到,Atomic*/Unsafe
提供的商店发布最常用于急切清空引用或在 producer/consumer 场景中,作为某些重要索引的优化消息传递机制。
根据您的具体要求,在 Java 中最好执行非易失性加载,然后可能是易失性加载。
您可以结合使用
int permits = theUnsafe.getInt(object, offset);
if (!enough(permits))
permits = theUnsafe.getVolatileInt(object, offset);
此模式可用于环形缓冲区,以最大限度地减少缓存行的流失。
volatile read 正是您要找的。
其实对应的volatile操作已经有了release/acquire语义(否则happens-before不可能成对的volatile write-read),但是成对的volatile操作不应该只是顺序一致(~happens-before ),但它们也应该在 total synchronization order 中,这就是为什么 StoreLoad
barrier 在 volatile write 之后插入:以保证 volatile 写入不同位置的总顺序,因此所有线程将以相同的顺序看到这些值.
易失性读取具有获取语义:proof from hotspot codebase, also there is direct recommendation by Doug Lea in JSR-133 cookbook(每次易失性读取后的 LoadLoad
和 LoadStore
障碍)。
Unsafe.loadFence()
也有 acquire 语义(proof), but used not to read value (you can do the same with plain volatile read), but to prevent reorder plain reads with subsequent volatile read. This is used in StampedLock 用于乐观阅读(参见 StampedLock#validate
方法实现和用法)。
在评论中讨论后更新。
让我们检查一下 Unsafe#loadStore()
和 volatile read 是否相同并且具有 acquire 语义。
我正在查看热点 C1 compiler source code 以避免通读 C2 中的所有优化。 它将字节码(实际上,不是字节码,而是它的解释器表示)转换为 LIR(低级中间表示),然后根据目标微体系结构将图形转换为实际操作码。
Unsafe#loadFence
是 intrinsic which has _loadFence
alias. In C1 LIR generator 它生成这个:
case vmIntrinsics::_loadFence :
if (os::is_MP()) __ membar_acquire();
其中 __
是用于生成 LIR 的宏。
现在让我们看看同一 LIR 生成器中的易失性读取 implementation。它尝试插入空值检查,检查 IRIW,检查我们是否在 x32 上并尝试读取 64 位值(用 SSE/FPU 创造一些魔法),最后,将我们引向相同的代码:
if (is_volatile && os::is_MP()) {
__ membar_acquire();
}
汇编程序生成器然后插入特定于平台的获取指令here。
看具体实现(这里没有链接,都可以在src/cpu/{$cpu_model}/vm/c1_LIRAssembler_{$cpu_model}.cpp中找到)
SPARC
void LIR_Assembler::membar_acquire() { // no-op on TSO }
x86
void LIR_Assembler::membar_acquire() { // No x86 machines currently require load fences }
Aarch64(弱内存模型,应该存在障碍)
void LIR_Assembler::membar_acquire() { __ membar(Assembler::LoadLoad|Assembler::LoadStore); }
根据 aarch architecture description 这样的 membar 将在加载后编译为
dmb ishld
指令。PowerPC(也是弱内存模型)
void LIR_Assembler::membar_acquire() { __ acquire(); }
然后转换为特定的 PowerPC 指令
lwsync
。根据 commentslwsync
在语义上等同于lwsync orders Store|Store, Load|Store, Load|Load, but not Store|Load
但只要 PowerPC 没有任何较弱的障碍,这是在 PowerPC 上实现获取语义的唯一选择。
结论
易失性读取和 Unsafe#loadFence()
在内存排序方面相同(但在可能的编译器优化方面可能不同),在最流行的 x86 上它是无操作的,PowerPC 是唯一支持的架构没有确切的获取障碍。