VarHandle通过将内存位置访问器与应用于它的内存顺序语义分离,推广了volatile访问。尽管volatile变量始终在每次读写时强制执行完全排序(顺序一致性),VarHandle提供四种不同模式——plain、opaque、acquire/release和volatile——允许开发人员在不需要完全顺序一致性时选择更弱的一致性模型。这种解耦使得高级并发算法能够在x86或ARM等架构上省略昂贵的StoreLoad屏障,在单生产者–单消费者队列等场景中显著提高吞吐量。该API在无需使用sun.misc.Unsafe的情况下实现了这一点,提供了一种完全支持的标准机制,用于堆外访问、数组元素操作和记录字段更新,具有精确、可验证的内存语义。
我们优化了一个用于遥测摄取的无锁环形缓冲区,其中生产者线程写入事件,消费者线程处理它们,二者都在共享的后备数组上操作。最初的实现使用volatile数组作为缓冲区元素,确保可见性,但在每次插槽更新时触发完全内存屏障,这在我们的ARM服务器上成为瓶颈。
考虑的第一个替代方案是保留volatile并添加缓存行填充,以避免虚假共享。这保存了正确性并减少了缓存一致性流量,但仍然施加了volatile固有的完全StoreLoad屏障成本,消耗了宝贵的CPU周期,用于生产者和消费者之间我们并不需要的排序保证。
我们评估了恢复到保护缓冲区索引的synchronized块,这将通过提供互斥简化安全推理。不幸的是,这种方法将生产者和消费者操作序列化,破坏了我们亚毫秒处理目标所必需的无锁延迟特性,并在重负载下引入了优先级反转风险。
我们采用了VarHandle,使用**setRelease进行生产者写入,使用getAcquire**进行消费者读取。这对提供了写入和随后的读取之间所需的先发生关系,而没有强制执行与其他变量的完全排序,完美匹配我们的单生产者–单消费者队列所需的内存模型。
在保持正确性的同时,结果吞吐量在ARM服务器上比volatile基线提高了约四十个百分点,证明当算法设计已经约束了并发模式时,更弱的一致性模型就足够了。
VarHandle是否仅仅是用于访问堆外内存的Unsafe的安全包装?
虽然VarHandle可以通过MemorySegment管理堆外段,但其主要的架构进步在于揭示内存排序模式,而Unsafe仅通过不透明屏障近似。VarHandle允许声明访问是否参与同步顺序(获取/释放)或仅提供原子性(不透明),这种区别在Unsafe的原始putOrdered中被混淆,或者需要手动插入屏障才能正确近似,使得代码对JMM的验证更加可靠。
setOpaque是否保证我的写入最终对另一个线程可见?
不可以。Opaque模式确保原子性和一致性——该写入在与同一变量的其他不透明访问之间看起来是不可分割和有序的——但没有提供线程间的先发生保证。使用**getOpaque读取的线程可能会无限循环观察一个过时的缓存值,除非某种其他同步机制强制刷新缓存,而acquire/release**则创建了写入者和读取者之间所需的可见性边缘。
我应该在什么情况下优先使用volatile模式而不是setRelease/getAcquire?
当您需要顺序一致性时,优先使用volatile:在全局同步顺序中,所有volatile操作之间的完全排序。使用acquire/release当您仅需要强制执行特定写入和随后的读取之间的排序(发布安全性),而不与所有其他内存访问进行协调。错误地将acquire/release应用于假设顺序一致性的算法会导致微妙的重排序错误,其中独立变量更新在不同观察者看来似乎无序地旋转。