【问题标题】:volatile barrier in Hibernate source code would "syncs state with other threads". How?Hibernate 源代码中的 volatile 屏障将“与其他线程同步状态”。如何?
【发布时间】:2018-04-26 15:58:49
【问题描述】:

我今天在挖掘hibernate-jpa的源代码,偶然发现了以下代码sn-p(你也可以找到here):

private static class PersistenceProviderResolverPerClassLoader implements PersistenceProviderResolver {

    //FIXME use a ConcurrentHashMap with weak entry
    private final WeakHashMap<ClassLoader, PersistenceProviderResolver> resolvers =
            new WeakHashMap<ClassLoader, PersistenceProviderResolver>();
    private volatile short barrier = 1;

    /**
     * {@inheritDoc}
     */
    public List<PersistenceProvider> getPersistenceProviders() {
        ClassLoader cl = getContextualClassLoader();
        if ( barrier == 1 ) {} //read barrier syncs state with other threads
        PersistenceProviderResolver currentResolver = resolvers.get( cl );
        if ( currentResolver == null ) {
            currentResolver = new CachingPersistenceProviderResolver( cl );
            resolvers.put( cl, currentResolver );
            barrier = 1;
        }
        return currentResolver.getPersistenceProviders();
    }

if ( barrier == 1 ) {} //read barrier syncs state with other threads 那个奇怪的说法让我感到不安。我花时间深入研究了volatile 关键字规范。

简单地说,在我的理解中,它确保了对相应变量的任何READWRITE 操作都将始终直接在内存中通常存储值的位置执行。它专门防止通过保存该值副本的缓存或注册器进行访问,并且不一定知道该值是否已更改或正在被另一个内核上的并发线程修改。

因此,它会导致性能下降,因为每次访问都意味着要一直进入内存,而不是使用通常的(流水线?)快捷方式。但它也确保了每当线程读取变量时,它始终是最新的。

我提供这些详细信息是为了让您知道我对关键字的理解。但是现在,当我重新阅读代码时,我告诉自己“好吧,我们通过确保始终为 1 的值始终为 1(并将其设置为 1)来减慢执行速度。这有什么帮助?"

谁能解释一下?

【问题讨论】:

  • 所有内存在访问 volatile 字段时被转储,搜索 SO 以找到 Java 规范的链接
  • 哦,我忘记在问题中写了。那么这有什么帮助呢?
  • 您的非易失性字段也将同步
  • 不,那不是“JPA 的源代码”。这是 HIBERNATE 的 JPA 实现中的一些代码。其他 JPA 提供者不这样做

标签: java multithreading hibernate jpa volatile


【解决方案1】:

你理解volatile错了。

它确保对相应的任何 READ 或 WRITE 操作 变量将始终直接在该位置的内存中执行 该值通常被存储。它专门防止通过 缓存或注册商持有值的副本,而不是 必须知道该值是否已更改或正在被 另一个内核上的并发线程。

您说的是实现,而实现可能因 jvm 不同而有所不同。


volatile很像某种规范或规则,它可以gurantee那个

写入 volatile 变量会建立发生前的关系 随后读取相同的变量。这意味着变化 到 volatile 变量始终对其他线程可见。什么是 此外,这也意味着当一个线程读取一个 volatile 变量时,它 不仅看到了 volatile 的最新变化,还看到了侧面 导致更改的代码的影响。

使用简单的原子变量访问比访问更高效 这些变量通过同步代码,但需要更加小心 程序员避免内存一致性错误。是否额外 努力是否值得取决于项目的规模和复杂性 应用。


在这种情况下,volatile 不用于保证barrier == 1

if ( barrier == 1 ) {} //read
PersistenceProviderResolver currentResolver = resolvers.get( cl );
if ( currentResolver == null ) {
    currentResolver = new CachingPersistenceProviderResolver( cl );
    resolvers.put( cl, currentResolver );
    barrier = 1; //write
}

它用于保证读写之间的副作用对其他线程是可见的。

没有它,如果你在 Thread1 的 resolvers 中放了一些东西,Thread2 可能不会注意到它。

有了它,如果 Thread2 在 Thread1 写入之后读取 barrier,则保证 Thread2 可以看到这个 put 动作。


而且,还有很多其他的同步机制,比如:

  • synchronized关键字
  • ReentrantLock
  • AtomicInteger
  • ....

通常,他们也可以在不同线程之间建立这种发生前发生的关系。

【讨论】:

  • 您的最后一个代码块没有正确识别,这可能会导致读者相信 if(barrier==1) 块内部发生了某些事情,但事实并非如此。
  • 为了完整起见,我建议您也引用这句话“使用简单的原子变量访问比通过同步代码访问这些变量更有效,但需要程序员更加小心避免内存一致性错误。额外的努力是否值得取决于应用程序的大小和复杂性“。实际上这是我试图理解的要点:那段代码降低了一致性错误的风险,但并不能完全防止它,我错了吗?
  • @Aldian 我刚刚更新了我的答案。而且,是的,它不会阻止它。
【解决方案2】:

这样做是为了通过建立 happen before 关系 (https://www.logicbig.com/tutorials/core-java-tutorial/java-multi-threading/happens-before.html) 将 resolvers 的更新映射到其他线程。

在单个线程中,以下指令在关系之前发生

resolvers.put( cl, currentResolver );
barrier = 1;

但是要使 resolvers 中的更改对其他线程可见,我们需要从 volatile 变量 barrier 中读取值,因为相同 volatile 变量的写入和后续读取发生在关系之前(这也是传递的)。所以基本上这是整体结果:

  1. 更新resolvers
  2. 写入易失性barrier
  3. 从 volatile barrier 读取以使在步骤 1 中进行的更新对从 barrier 读取值的线程可见

【讨论】:

  • 你说的很有道理。除了有人可能想知道是否没有比执行if 更简单的读取barrier 的方法。而且我仍然不相信同时执行相同操作的两个线程不能同时执行在barrier = 1; 语句之前调用resolvers.put( cl, currentResolver );
  • @Aldian if 可以替换为short dummy = barrier;,但不确定这会比if 提出更少的问题。是的,如果两个线程将同时执行,则将创建两个对象new CachingPersistenceProviderResolver( cl )。为避免使用 ConcurrentHashMapsynchronized 块。
【解决方案3】:

Volatile variables - 是 Java 中的轻量级同步形式。

声明一个字段volatile会产生以下效果:

  • 编译器不会重新排序操作
  • 变量不会在寄存器中兑现
  • 对 64 位数据结构的操作将作为原子操作执行
  • 会影响其他变量的可见性同步

引自 Brian Goetz 的 Concurrency in practice

volatile 变量的可见性影响超出了值 volatile 变量本身。当线程 A 写入 volatile 变量,随后线程 B 读取相同的变量, 在写入之前对 A 可见的所有变量的值 volatile 变量在读取 volatile 后对 B 可见 变量。

好的,保留1 而不将resolvers 声明为volatile WeakHashMap 有什么意义?

此安全发布保证仅适用于原始字段和对象引用。出于此可见性保证的目的,实际成员是对象引用; volatile 对象引用所引用的对象超出了安全发布保证的范围。因此,将对象引用声明为 volatile 不足以保证对引用对象成员的更改会发布到其他线程。一个线程可能无法观察到另一个线程最近对此类对象引用的成员字段的写入。

此外,当引用对象是可变的并且缺乏线程安全时,其他线程可能会看到部分构造的对象或处于不一致状态的对象。

Map 对象的实例是可变的,因为它的 put() 方法。

get()put() 的交错调用可能会导致从Map 对象中检索到内部不一致的值,因为put() 修改了它的状态。声明对象引用 volatile 不足以消除这种数据竞争。

由于volatile变量建立了happens-before关系,当一个线程有更新时,它只是可以通知其他访问barrier的人。

从内存可见性的角度来看,写一个 volatile 变量就像退出同步块并读取易失性 变量就像进入一个同步块。

【讨论】:

    猜你喜欢
    • 2021-03-19
    • 2014-03-12
    • 2016-06-22
    • 1970-01-01
    • 2019-04-09
    • 1970-01-01
    • 2021-08-25
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多