【问题标题】:Does re-putting an object into a ConcurrentHashMap cause a "happens-before" memory relation?将对象重新放入 ConcurrentHashMap 是否会导致“先发生”内存关系?
【发布时间】:2011-10-18 14:43:20
【问题描述】:

我正在使用 现有 代码,该代码具有 ConcurrentHashMap 形式的对象存储。映射中存储了可变对象,供多个线程使用。没有两个线程试图通过设计一次修改一个对象。我关心的是线程之间修改的可见性。

目前,对象的代码在“setter”(由对象本身保护)上具有同步性。 “getter”上没有同步,成员也不是易变的。对我来说,这意味着无法保证可见性。但是,当一个对象被修改时,它会重新放置回到映射中(再次调用put() 方法,相同的键)。这是否意味着当另一个线程将对象拉出映射时,它会看到修改?

我在 stackoverflow、JCIP 和 java.util.concurrent 的包描述中对此进行了研究。我想我基本上把自己弄糊涂了……但导致我问这个问题的最后一根稻草来自包装描述,它指出:

将对象放入任何并发集合之前线程中的操作发生在另一个线程中从集合中访问或删除该元素之后的操作。

关于我的问题,“动作”是否包括在 re-put() 之前对存储在地图中的对象的修改?如果所有这些确实导致跨线程的可见性,这是​​一种有效的方法吗?我对线程比较陌生,非常感谢您的 cmets。

编辑:

谢谢大家的回复!这是我在 StackOverflow 上的第一个问题,对我很有帮助。

我必须接受ptomli 的回答,因为我认为它最清楚地解决了我的困惑。也就是说,在这种情况下,建立“之前发生”关系并不一定会影响修改可见性。关于文本中描述的实际问题,我的“标题问题”构造不佳。 ptomli 的答案现在与我在 JCIP 中读到的内容不谋而合:“为了确保所有线程都能看到共享可变变量的最新值,读写线程必须在一个公共锁上同步”(第37)。将对象重新放入映射中不会为修改插入对象的成员提供这种通用锁。

我很欣赏所有关于改变的技巧(不可变对象等),我完全同意。但是对于这种情况,正如我提到的,由于仔细的线程处理,没有并发修改。一个线程修改一个对象,另一个线程稍后读取该对象(CHM 是对象传送器)。鉴于我提供的情况,我认为 CHM 不足以确保稍后执行的线程将看到第一个的修改。但是,我认为你们中的许多人正确回答了标题问题

【问题讨论】:

  • 确实如此,它会锁定任何修改。

标签: java concurrency concurrenthashmap


【解决方案1】:

在每次写入对象后调用concurrHashMap.put。但是,您没有指定在每次阅读之前还要调用 concurrHashMap.get。这是必要的。

这适用于所有形式的同步:您需要在两个线程中都有一些“检查点”。只同步一个线程是没用的。

我还没有检查 ConcurrentHashMap 的源代码以确保 putget 触发之前发生,但它们应该是合乎逻辑的。

即使您同时使用putget,您的方法仍然存在问题。当您修改一个对象并在它为put 之前被另一个线程使用(处于不一致状态)时,就会出现问题。这是一个微妙的问题,因为您可能认为旧值会被读取,因为它还不是put,它不会引起问题。问题是当你不同步时,你不能保证得到一个一致的旧对象,而是行为是未定义的。 JVM 可以随时更新其他线程中对象的任何部分。只有在使用某些显式同步时,您才能确定以一致的方式跨线程更新值。

你可以做什么:
(1) 在代码中随处同步对对象的所有访问(getter 和 setter)。小心设置器:确保不能将对象设置为不一致的状态。例如,在设置名字和姓氏时,只有两个同步的设置器是不够的:您必须同时为两个操作获取对象锁。

(2)当你put一个对象在map中时,放一个深拷贝而不是对象本身。这样其他线程将永远不会读取处于不一致状态的对象。

编辑
我刚注意到

目前对象的代码在“setter”上同步 (由对象本身保护)。上没有同步 “getter”也不是易变的成员。

这不好。正如我上面所说,仅在一个线程上同步根本不是同步。您可能会在所有编写器线程上同步,但谁在乎,因为读者不会获得正确的值。

【讨论】:

    【解决方案2】:

    我认为这已经在几个答案中说过,但总结一下

    如果你的代码成功了

    • CHM#get
    • 调用各种设置器
    • CHM#put

    那么 put 提供的“happens-before”将保证所有 mutate 调用在 put 之前执行。这意味着任何后续获取都将保证看到这些更改。

    您的问题是对象的实际状态将不是确定性的,因为如果事件的实际流是

    • 线程 1:CHM#get
    • 线程 1:调用设置器
    • 线程 2:CHM#get
    • 线程 1:调用设置器
    • 线程 1:调用设置器
    • 线程 1:CHM#put

    那么无法保证对象在线程 2 中的状态。它可能会看到具有第一个 setter 提供的值的对象,也可能不会。

    不可变副本将是最好的方法,因为这样只会发布完全一致的对象。使各种 setter 同步(或底层引用 volatile)仍然不能让您发布一致的状态,这只是意味着对象将始终在每次调用时看到每个 getter 的最新值。

    【讨论】:

      【解决方案3】:

      我认为您的问题更多地与您存储在地图中的对象以及它们对并发访问的反应有关,而不是并发地图本身。

      如果您存储在映射中的实例具有同步的修改器,但没有同步的访问器,那么我看不出它们如何像描述的那样是线程安全的。

      Map 排除在外,并确定您存储的实例本身是否是线程安全的。

      但是,当一个对象被修改时,它会被重新放回映射中(再次调用 put() 方法,相同的键)。这是否意味着当另一个线程将对象拉出映射时,它会看到修改?

      这说明了混乱。重新放入 Map 的实例将由另一个线程从 Map 中检索。这是并发地图的保证。这与存储实例本身状态的可见性无关。

      【讨论】:

      • 也许我在问一些愚蠢的问题,但为什么要重新提出?其他线程是否正在删除该实例?
      • @Mister Smith 重新提出是触发发生之前(刷新缓存),以便所有线程都能看到更新的值。但是,我认为它并没有达到预期的效果(请参阅我的回答)。
      • @toto2 你在那里使用的奇怪逻辑。相同的实例被放回相同的键中。映射不会影响存储实例的状态变化的可见性。映射做出的并发保证只是从 get 中返回正确的实例(例如)。正如我所指出的,OP 问题确实与存储实例的线程安全有关。地图只是一种消遣。至于为什么作者认为他们在重新投入?谁知道,但可能不会按照他们的想法去做。
      • @ptomli 我不同意你的第二句话。在最新的 java 内存模型中,当有“happens-before”时,所有内容都会更新。尽管如此,即使所有内容都已更新,同步过程仍然存在缺陷,正如我在回答中所解释的那样。
      【解决方案4】:

      我的理解是它应该适用于重新放置后的所有获取,但这将是一种非常不安全的同步方法。

      在重新放置之前会发生什么,但在修改发生时。他们可能只看到一些变化,并且对象的状态会不一致。

      如果可以,我建议在地图中存储不可变对象。然后,任何 get 都会检索它执行 get 时的当前对象版本。

      【讨论】:

        【解决方案5】:

        这是来自java.util.concurrent.ConcurrentHashMap(Open JDK 7)的代码 sn-p:

          919       public V get(Object key) {
          920           Segment<K,V> s; // manually integrate access methods to reduce overhead
          921           HashEntry<K,V>[] tab;
          922           int h = hash(key.hashCode());
          923           long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE;
          924           if ((s = (Segment<K,V>)UNSAFE.getObjectVolatile(segments, u)) != null &&
          925               (tab = s.table) != null) {
          926               for (HashEntry<K,V> e = (HashEntry<K,V>) UNSAFE.getObjectVolatile
          927                        (tab, ((long)(((tab.length - 1) & h)) << TSHIFT) + TBASE);
          928                    e != null; e = e.next) {
          929                   K k;
          930                   if ((k = e.key) == key || (e.hash == h && key.equals(k)))
          931                       return e.value;
          932               }
          933           }
          934           return null;
          935       }
        

        UNSAFE.getObjectVolatile()documented 作为getter,内部有volatile 语义,因此在获取引用时会跨越内存屏障。

        【讨论】:

          【解决方案6】:

          是的,put 会发生易失性写入,即使映射中已经存在键值。

          使用 ConcurrentHashMap 跨线程发布对象非常有效。对象在地图中后不应进一步修改。 (它们不必严格不可变(带有最终字段))

          【讨论】:

          • 我认为他的代码确实会在对象最初放入地图后对其进行修改。我的回答是将对象的深层副本放在地图中,这样就不会被修改。
          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多