9

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

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

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

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

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

编辑:

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

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

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

4

6 回答 6

7

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

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

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

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

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

(2)当您put在地图中放置一个对象时,放置一个深层副本而不是对象本身。这样,其他线程将永远不会读取处于不一致状态的对象。

编辑
我刚刚注意到

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

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

于 2011-10-18T15:11:08.097 回答
5

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

如果你的代码去

  • CHM#获取
  • 调用各种设置器
  • CHM#put

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

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

  • 线程1:CHM#get
  • 线程1:调用setter
  • 线程 2: CHM#get
  • 线程1:调用setter
  • 线程1:调用setter
  • 线程 1:CHM#put

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

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

于 2011-10-18T16:37:14.783 回答
4

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

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

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

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

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

于 2011-10-18T14:51:35.590 回答
3

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

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

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

于 2011-10-18T15:02:40.300 回答
2

java.util.concurrent.ConcurrentHashMap这是(Open JDK 7)的代码片段 :

  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()记录为具有内部volatile语义的 getter,因此在获取引用时将跨越内存屏障。

于 2011-10-18T16:00:59.673 回答
1

put的,即使键值已存在于映射中,也会导致易失性写入。

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

于 2011-10-18T15:26:28.853 回答