在 Java 中模拟内存屏障以消除易失性读取
Emulating a memory barrier in Java to get rid of volatile reads
假设我有一个被并发访问的字段,它被多次读取而很少写入。
public Object myRef = new Object();
假设线程 T1 将每分钟一次将 myRef 设置为另一个值,而其他 N 个线程将连续并发地读取 myRef 数十亿次。我只需要 myRef 最终对所有线程可见。
一个简单的解决方案是使用 AtomicReference 或像这样简单的 volatile:
public volatile Object myRef = new Object();
但是,afaik 易失性读取确实会产生性能成本。我知道它很小,这更像是我想知道的东西,而不是我真正需要的东西。因此,让我们不要关心性能并假设这是一个纯粹的理论问题。
所以问题归结为:有没有办法通过在写入站点做一些事情来安全地绕过很少写入的引用的易失性读取?
经过一番阅读,记忆障碍似乎正是我所需要的。因此,如果存在这样的构造,我的问题就会得到解决:
- 写
- 调用屏障(同步)
- 一切都已同步,所有线程都将看到新值。 (在读取站点没有永久性成本,它可能会过时或在缓存同步时产生一次性成本,但在那之后它会全部恢复到常规字段获取直到下一次写入)。
在Java中是否有这样的结构,或者一般情况下?在这一点上,我忍不住想,如果存在这样的东西,它应该已经被更聪明的维护者合并到原子包中了。 (不成比例地频繁读取与写入可能不是一个需要关心的案例?)所以也许我的想法有问题,这样的结构根本不可能?
我看到一些代码示例将 'volatile' 用于类似的目的,利用它的先发生契约。有一个单独的同步字段,例如:
public Object myRef = new Object();
public volatile int sync = 0;
并在写作 thread/site:
myRef = new Object();
sync += 1 //volatile write to emulate barrier
我不确定这是否有效,有些人认为这仅适用于 x86 架构。在阅读了 JMS 中的相关部分之后,我认为只有当易失性写入与需要查看 myRef 的新值的线程的易失性读取相结合时,它才能保证工作。 (所以不要摆脱易失性读取)。
回到我原来的问题;这可能吗? Java 有可能吗? Java 9 VarHandles 中的新 API 之一是否可行?
不太确定这是否正确,但我可能会使用队列来解决这个问题。
创建一个包装 ArrayBlockingQueue 属性的 class。 class 有一个更新方法和一个读取方法。 update 方法将新值发布到队列中并删除除最后一个值之外的所有值。读取方法 returns 对队列进行查看操作的结果,即读取但不删除。查看队列前端元素的线程可以畅通无阻。更新队列的线程干净利落。
所以基本上你想要 volatile
的语义而不需要运行时成本。
我觉得不可能。
问题是 volatile
的运行时成本是由于在编写器和 reader 代码中实现内存屏障的指令。如果您 "optimize" reader 通过摆脱它的内存屏障,那么您不再保证 reader 在实际写入时会看到 "seldomly written" 新值。
FWIW,sun.misc.Unsafe
class 的某些版本提供了明确的 loadFence
、storeFence
和 fullFence
方法,但我不认为使用它们将比使用 volatile
.
提供任何性能优势
假设 ...
您希望多处理器系统中的一个处理器能够告知所有其他处理器:
"Hey! Whatever you are doing, invalidate your memory cache for address XYZ, and do it now."
遗憾的是,现代 ISA 不支持这一点。
实际上,每个处理器控制自己的缓存。
- 您可以使用
ReentrantReadWriteLock
,它专为少写多读场景设计。
你可以使用StampedLock
,它是为同样的写少读多的情况设计的,但也可以乐观地尝试读。示例:
private StampedLock lock = new StampedLock();
public void modify() { // write method
long stamp = lock.writeLock();
try {
modifyStateHere();
} finally {
lock.unlockWrite(stamp);
}
}
public Object read() { // read method
long stamp = lock.tryOptimisticRead();
Object result = doRead(); //try without lock, method should be fast
if (!lock.validate(stamp)) { //optimistic read failed
stamp = lock.readLock(); //acquire read lock and repeat read
try {
result = doRead();
} finally {
lock.unlockRead(stamp);
}
}
return result;
}
使您的状态 immutable 并仅通过克隆现有对象并通过构造函数仅更改必要的属性来允许受控修改。构建新状态后,将其分配给许多读取线程正在读取的引用。这样 读取线程会产生零成本 .
X86提供TSO;您免费获得 [LoadLoad][LoadStore][StoreStore] 栅栏。
易失性读取需要释放语义。
r1=Y
[LoadLoad]
[LoadStore]
...
如您所见,X86 已经免费提供了。
在您的情况下,大多数调用都是读取,并且缓存行已经在本地缓存中。
编译器级别的优化需要付出代价,但在硬件级别,易失性读取与常规读取一样昂贵。
另一方面,volatile 写入更昂贵,因为它需要 [StoreLoad] 来保证顺序一致性(在 JVM 中,这是使用 lock addl %(rsp),0
或 MFENCE 完成的)。由于在您的情况下很少写入,因此这不是问题。
我会谨慎处理此级别的优化,因为很容易使代码比实际需要的更复杂。最好通过一些基准来指导您的开发工作,例如使用 JMH 并最好在真实硬件上进行测试。也可能隐藏着其他令人讨厌的生物,例如虚假共享。
假设我有一个被并发访问的字段,它被多次读取而很少写入。
public Object myRef = new Object();
假设线程 T1 将每分钟一次将 myRef 设置为另一个值,而其他 N 个线程将连续并发地读取 myRef 数十亿次。我只需要 myRef 最终对所有线程可见。
一个简单的解决方案是使用 AtomicReference 或像这样简单的 volatile:
public volatile Object myRef = new Object();
但是,afaik 易失性读取确实会产生性能成本。我知道它很小,这更像是我想知道的东西,而不是我真正需要的东西。因此,让我们不要关心性能并假设这是一个纯粹的理论问题。
所以问题归结为:有没有办法通过在写入站点做一些事情来安全地绕过很少写入的引用的易失性读取?
经过一番阅读,记忆障碍似乎正是我所需要的。因此,如果存在这样的构造,我的问题就会得到解决:
- 写
- 调用屏障(同步)
- 一切都已同步,所有线程都将看到新值。 (在读取站点没有永久性成本,它可能会过时或在缓存同步时产生一次性成本,但在那之后它会全部恢复到常规字段获取直到下一次写入)。
在Java中是否有这样的结构,或者一般情况下?在这一点上,我忍不住想,如果存在这样的东西,它应该已经被更聪明的维护者合并到原子包中了。 (不成比例地频繁读取与写入可能不是一个需要关心的案例?)所以也许我的想法有问题,这样的结构根本不可能?
我看到一些代码示例将 'volatile' 用于类似的目的,利用它的先发生契约。有一个单独的同步字段,例如:
public Object myRef = new Object();
public volatile int sync = 0;
并在写作 thread/site:
myRef = new Object();
sync += 1 //volatile write to emulate barrier
我不确定这是否有效,有些人认为这仅适用于 x86 架构。在阅读了 JMS 中的相关部分之后,我认为只有当易失性写入与需要查看 myRef 的新值的线程的易失性读取相结合时,它才能保证工作。 (所以不要摆脱易失性读取)。
回到我原来的问题;这可能吗? Java 有可能吗? Java 9 VarHandles 中的新 API 之一是否可行?
不太确定这是否正确,但我可能会使用队列来解决这个问题。
创建一个包装 ArrayBlockingQueue 属性的 class。 class 有一个更新方法和一个读取方法。 update 方法将新值发布到队列中并删除除最后一个值之外的所有值。读取方法 returns 对队列进行查看操作的结果,即读取但不删除。查看队列前端元素的线程可以畅通无阻。更新队列的线程干净利落。
所以基本上你想要 volatile
的语义而不需要运行时成本。
我觉得不可能。
问题是 volatile
的运行时成本是由于在编写器和 reader 代码中实现内存屏障的指令。如果您 "optimize" reader 通过摆脱它的内存屏障,那么您不再保证 reader 在实际写入时会看到 "seldomly written" 新值。
FWIW,sun.misc.Unsafe
class 的某些版本提供了明确的 loadFence
、storeFence
和 fullFence
方法,但我不认为使用它们将比使用 volatile
.
假设 ...
您希望多处理器系统中的一个处理器能够告知所有其他处理器:
"Hey! Whatever you are doing, invalidate your memory cache for address XYZ, and do it now."
遗憾的是,现代 ISA 不支持这一点。
实际上,每个处理器控制自己的缓存。
- 您可以使用
ReentrantReadWriteLock
,它专为少写多读场景设计。 你可以使用
StampedLock
,它是为同样的写少读多的情况设计的,但也可以乐观地尝试读。示例:private StampedLock lock = new StampedLock(); public void modify() { // write method long stamp = lock.writeLock(); try { modifyStateHere(); } finally { lock.unlockWrite(stamp); } } public Object read() { // read method long stamp = lock.tryOptimisticRead(); Object result = doRead(); //try without lock, method should be fast if (!lock.validate(stamp)) { //optimistic read failed stamp = lock.readLock(); //acquire read lock and repeat read try { result = doRead(); } finally { lock.unlockRead(stamp); } } return result; }
使您的状态 immutable 并仅通过克隆现有对象并通过构造函数仅更改必要的属性来允许受控修改。构建新状态后,将其分配给许多读取线程正在读取的引用。这样 读取线程会产生零成本 .
X86提供TSO;您免费获得 [LoadLoad][LoadStore][StoreStore] 栅栏。
易失性读取需要释放语义。
r1=Y
[LoadLoad]
[LoadStore]
...
如您所见,X86 已经免费提供了。
在您的情况下,大多数调用都是读取,并且缓存行已经在本地缓存中。
编译器级别的优化需要付出代价,但在硬件级别,易失性读取与常规读取一样昂贵。
另一方面,volatile 写入更昂贵,因为它需要 [StoreLoad] 来保证顺序一致性(在 JVM 中,这是使用 lock addl %(rsp),0
或 MFENCE 完成的)。由于在您的情况下很少写入,因此这不是问题。
我会谨慎处理此级别的优化,因为很容易使代码比实际需要的更复杂。最好通过一些基准来指导您的开发工作,例如使用 JMH 并最好在真实硬件上进行测试。也可能隐藏着其他令人讨厌的生物,例如虚假共享。