【问题标题】:Safe publication and ConcurrentHashMap安全发布和 ConcurrentHashMap
【发布时间】:2020-07-09 20:40:15
【问题描述】:

假设我们有一个类 Container

class Container {
      private final Map<LongHolder, Integer> map = new ConcurrentHashMap<>();
      static class LongHolder {
         private final Long i;
         private LongHolder(Long i) {
            this.i = i;
         }
      }

      public LongHolder createValue(Long i) {
          LongHolder v = new LongHolder(i);
          map.put(v, 1)
          return LongHolder;
      }

      public void doSmth(LongHolder longHolder) {
         map.get(longHolder);
         ... // do smth
      }
}

是否可以以这样的方式对操作进行重新排序,即从 createValue 对 LongHolder 的引用会转义该方法,从而在将其放入映射之前对其他线程可用?在这种情况下,我们可以获取 LongHolder 引用表单 createValue 并将其传递给在地图中看不到的另一个线程中的 doSmth。可能吗?如果不是请解释一下。

更新: ConcurrentHashMap 的 Javadoc 状态 Retrievals 反映了最近完成的更新操作的结果。 发作。 (更正式地说,给定密钥的更新操作带有 与任何(非空)检索的发生之前的关系 该键报告更新的值。) 但是有最近完成的更新操作 重新排序时的问题正是关于这种微妙的情况 可能会以实际更新操作的方式发生 在put.The Javadoc 之前未完成并且引用已转义 ConcurrentHashMap 仅说明 给定密钥具有 与该键的任何(非空)检索发生之前的关系 换句话说,只有当我们检索到那个键时,我们才能辩论 关于发生在与此键的更新操作的关系。

我只能提出一些其他无法实现的原因:

1) 重新排序规则比我想象的更严格,我们不能假设仲裁重新排序(或者我们可以吗?规则是什么?)。

2) put 具有一些不允许重新编码的神奇属性,而 return 仅在完成 ​​put 后才会发生。在这种情况下,这些属性是什么?

【问题讨论】:

  • 不,这是不可能的。 ConcurrentHashMap 保证 Happens-before relationship。所以,只要put 发生在get 之前,就会得到一致的结果。
  • 你如何想象它“逃脱了方法”?它在那里创建然后返回;除了createValue 的结果,即在它已经放入地图之后,另一段代码如何访问它?
  • 在没有适当同步的情况下,在对象构造过程中也会发生同样的情况。

标签: java concurrency java.util.concurrent


【解决方案1】:

假设线程AB 引用了Container 实例。

(由于Container.map被声明为final,它的值将被安全地发布。此外,由于它引用ConcurrentHashMap,因此不需要同步来维护线程安全。)

现在假设线程A 调用Longholder holder = container.createValue(x) 并以某种方式将holder 传递给线程B。你的问题是,如果B 调用doSmth 通过该持有人,map.get(holder) 调用会在地图中看到它吗?

答案是肯定的。

ConcurrentHashMap 的规范是这样说的:

“检索反映了最近完成的更新操作在其开始时的结果。(更正式地说,给定键的更新操作与任何(非空)检索具有发生前的关系键报告更新的值。)”

这意味着在put 调用该线程A 和随后的get 调用线程B 之前发生。这反过来意味着:

  • get找到LongHolder键,并且
  • get返回正确的值。

LongHolder 对象的 i 字段的值也将符合预期,即使 LongHolder可变 持有者也是如此。

(请注意,在这里声明一个不可变的LongHolder 并没有多大意义。它等效于java.lang.Long ...尽管API 更简单。)


能否以这样一种方式重新排序操作,以使来自createValueLongHolder 引用转义该方法,从而在将其放入映射之前可供其他线程使用?

基本上没有。相关操作是这样的:

      LongHolder v = new LongHolder(i);
      map.put(v, 1)
      return v;    // corrected typo
  1. 没有任何意义的源代码重新排序。
  2. 如果您正在讨论编译器对正在运行的线程进行重新排序createValue,JLS 不允许这样做。禁止任何改变线程内可见性的重新排序。
  3. 由于getput 的属性,通过地图发布v 是安全的。 (在实现时,内存屏障禁止在 getput 调用周围进行有害的重新排序。)

  4. 和 3. 是 发生在关系之前的结果

现在,如果代码要改成这样:

      LongHolder v = new LongHolder(i);
      map.put(v, 1)
      // Somehow modify v.i
      return v;

那么将是一种不安全的发布形式。由于Av.i 的更改发生在put 之后,因此putBget 之间的发生在 关系不足以保证使v.i 的新值在线程 B 中可见。那是因为 链接 不再起作用了。

现在,我想如果线程AcreateValue 调用的结果以不安全的方式传递给另一个线程(BC),则不能保证后者看到v.i 的正确值 ...如果 LongHolder 是可变的。但这不是 createValue / doSmth 代码的问题。并且通过地图发布v的值是安全的。

但我认为这个关于重新排序的讨论没有抓住重点。任何违反内存模型保证的可见性语义的重新排序都是被禁止的。 JIT 编译器不允许这样做。所以你只需要做 happens before 分析。

【讨论】:

  • 感谢您的解释。我已经更新了我的问题,因为答案不适合评论的大小。
猜你喜欢
  • 1970-01-01
  • 2014-03-04
  • 2013-02-03
  • 1970-01-01
  • 2012-08-20
  • 1970-01-01
  • 1970-01-01
  • 2019-09-22
  • 2011-04-15
相关资源
最近更新 更多