在Java中,我可以依靠参考分配是原子上写落实副本?(In Java can I depend on

2019-06-27 02:08发布

如果我有一个多线程环境中不同步的Java集合,我不想强迫收集的读者同步[1],是我同步作家和使用引用赋值的可行的原子性的解决方案? 就像是:

private Collection global = new HashSet(); // start threading after this

void allUpdatesGoThroughHere(Object exampleOperand) {
  // My hypothesis is that this prevents operations in the block being re-ordered
  synchronized(global) {
    Collection copy = new HashSet(global);
    copy.remove(exampleOperand);
    // Given my hypothesis, we should have a fully constructed object here. So a 
    // reader will either get the old or the new Collection, but never an 
    // inconsistent one.
    global = copy;    
  }
}

// Do multithreaded reads here. All reads are done through a reference copy like:
// Collection copy = global;
// for (Object elm: copy) {...
// so the global reference being updated half way through should have no impact 

滚动自己的解决方案似乎经常在这些类型的情况下失败了,所以我有兴趣知道我可以用它来防止对象的创建等图案,集合或库,并阻止我的数据的消费者。


[1]对时间的相当大的比例的原因花费在读出相比写入,结合引入死锁的危险。


编辑:在几个答案和意见,一些重要的点了很多很好的信息:

  1. 发生错误,目前在我发布的代码。 同步全球(一个严重命名变量)可能会失败交换后,以保护syncronized块。
  2. 你可以通过在类同步(移动synchronized关键字的方法)解决这个问题,但可能有其他错误。 一个更安全,更易于维护的解决方案是使用从java.util.concurrent的东西。
  3. 有没有“最终一致性保证”在我发布的代码,以确保读者不要让作家才能看到更新的一种方式是使用volatile关键字。
  4. 上的反映,促使这个问题,试图实现无锁读取与写入锁定在Java中,但我的(解决)问题是与集合,它可能会不必要地混淆为未来的读者普遍的问题。 所以,如果这不是明摆着我通过允许一个作家在同一时间发布的作品的代码来执行编辑“一些对象”,即由多个读线程读取保护。 编辑的承诺是通过原子操作完成,以便读者只能得到预先编辑或编辑后“对象”。 当/如果读者线程得到更新,它不能作为读取发生的“对象”的旧副本读的中间发生。 这可能已经被发现和证明了一个简单的解决方案在之前的Java中更好的并发支持的可用性某种方式被打破。

Answer 1:

而不是试图推出自己的解决方案,为什么不使用的ConcurrentHashMap为您所设定的,只是将所有的值给部分标准值? (如恒定Boolean.TRUE将工作做好。)

我想,这与实现的众多阅读器,为数不多的作家场景效果很好。 甚至还有一个构造函数,可以让你设定了预期的“并发级别” 。

更新:德维尔已经使用建议Collections.newSetFromMap实用的方法来打开的ConcurrentHashMap成一组。 由于该方法需要一个Map<E,Boolean>我的猜测是,它做同样的事情与设置所有值Boolean.TRUE幕后的。


更新:解决了海报的榜样

这可能就是我将最终去,但我还是很好奇我的极简主义的解决方案如何能够失败。 - MilesHampson

你简约的解决方案会工作得很好了一些调整。 我担心的是,虽然它现在是最小的,它可能会在未来更复杂。 很难记住所有你认为使一些线程安全的,特别是如果你回来的代码周/月/年之后做出看似不起眼的调整时的条件。 如果做的ConcurrentHashMap你需要有足够的性能都那么为什么不使用呢? 所有讨厌的并发细节被封装远,甚至6个月 - 从 - 现在你将有一个很难搞乱吧!

你至少需要一个好办法之前,当前的解决方案会奏效。 正如已经指出的那样,你也许应该补充的volatile改性剂global的声明。 我不知道,如果你有一个C / C ++背景,但我很惊讶,当我得知的语义volatile 在Java中其实比复杂得多用C 。 如果你做了很多在Java中并发编程的计划,然后它会是一个好主意,熟悉的基础知识, Java存储模型 。 如果你不作参考globalvolatile引用那么它可能没有线程将看到的价值的任何改变global直到他们尝试更新它,此时进入synchronized块将刷新本地缓存,并得到更新后的基准值。

然而,即使添加的volatile还是有一个巨大的问题。 这里有一个问题方案有两个线程:

  1. 我们开始与空集,或global={} 线程AB都在其线程本地缓存的内存这个值。
  2. 线程A取得取得synchronized的锁定global ,并通过制作一个拷贝的更新global和添加新的密钥来设置的。
  3. 虽然螺纹A仍然是内部synchronized块,螺纹B读取其本地值global入堆栈,试图进入synchronized块。 由于螺纹A是目前监视线程内部B块。
  4. 螺纹A通过设置参考和离开显示器,从而导致完成更新global={1}
  5. 螺纹B现在能够进入显示器,使副本global={1}集。
  6. 主题A决定再进行一次更新,在其本地读取global参考,并试图进入synchronized块。 由于线程B目前持有的锁{}有上没有锁{1}和线程A成功进入显示器!
  7. 线程A也使得副本{1}进行更新的目的。

现在螺纹AB都是内部synchronized块和它们具有的相同副本global={1}集。 这意味着他们的更新之一将丢失! 这种情况是由你同步存储在你正在你的内部更新的参考对象的事实引起的synchronized块。 你应该总是非常小心你使用同步的对象。 您可以通过添加一个新的变量来充当锁解决这个问题:

private volatile Collection global = new HashSet(); // start threading after this
private final Object globalLock = new Object(); // final reference used for synchronization

void allUpdatesGoThroughHere(Object exampleOperand) {
  // My hypothesis is that this prevents operations in the block being re-ordered
  synchronized(globalLock) {
    Collection copy = new HashSet(global);
    copy.remove(exampleOperand);
    // Given my hypothesis, we should have a fully constructed object here. So a 
    // reader will either get the old or the new Collection, but never an 
    // inconsistent one.
    global = copy;    
  }
}

此错误是阴险的,以至于没有其他的答案尚未解决它。 正是这些各种疯狂的并发细节,导致我使用的东西从已调试的java.util.concurrent库建议,而不是试图把东西一起自己。 我认为,上述方案将工作,但它是多么容易再次搞砸了? 这将是容易得多:

private final Set<Object> global = Collections.newSetFromMap(new ConcurrentHashMap<Object,Boolean>());

由于基准是final ,你不必担心使用陈旧引用线程,因为ConcurrentHashMap内部处理所有讨厌的内存模型问题,你不必担心所有显示器及记忆力障碍的讨厌的细节!



Answer 2:

据相关Java教程 ,

我们已经看到,增量表达式,如c++ ,没有描述原子动作。 即使是非常简单的表达式可以定义分解为其他动作复杂的动作。 不过,也有可以指定是原子操作:

  • 读取和写入是原子参考变量和最原始的变量(所有类型的不同之处longdouble )。
  • 读取和写入原子的所有变量声明volatile包括 longdouble变量)。

这是由重申了Java语言规范的第§17.7

写入和读出的引用总是原子,而不管它们是否被实现为32位或64位值。

看来,你确实可以依靠参考访问是原子; 然而,认识到,这并不能保证所有的读者会读的更新值global此次写操作之后-即这里没有内存排序保证。

如果您使用通过隐锁synchronized对所有进入global ,那么你可以在这里建立了一些内存一致性......但它可能是最好使用另一种方法。

你似乎也想在收集global保持不变......幸运的是, Collections.unmodifiableSet ,你可以用它来执行这一点。 举个例子,你应该会做类似下面的...

private volatile Collection global = Collections.unmodifiableSet(new HashSet());

......这,或使用AtomicReference

private AtomicReference<Collection> global = new AtomicReference<>(Collections.unmodifiableSet(new HashSet()));

那么你可以使用Collections.unmodifiableSet您的修改拷贝。


// ... All reads are done through a reference copy like:
// Collection copy = global;
// for (Object elm: copy) {...
// so the global reference being updated half way through should have no impact

你应该知道,在复印这里是多余的,因为内部for (Object elm : global)创建一个Iterator如下...

final Iterator it = global.iterator();
while (it.hasNext()) {
  Object elm = it.next();
}

因此,没有切换到一个完全不同的价值机会global在阅读之中。


所有这一切不谈,我与同意由道韫表示情绪 ...有你在这里滚动你自己的数据结构中的任何原因时,有可能是在提供替代java.util.concurrent ? 我想也许你正在处理一个旧的Java,因为您使用原始类型,但它不会伤害要求。

你可以找到提供写入时复制语义收集CopyOnWriteArrayList ,或者其表弟CopyOnWriteArraySet (它实现了一个Set使用前者)。


还通过道韫建议 ,你有没有考虑过使用一个ConcurrentHashMap ? 他们保证使用for循环正如您在您的例子做将是一致的。

类似地,迭代器和枚举返回反射在或自创建迭代器/枚举的哈希表的在某一时刻的状态的元件。

在内部, Iterator被用于增强for超过一个Iterable

您可以制作一个Set从这利用Collections.newSetFromMap像如下:

final Set<E> safeSet = Collections.newSetFromMap(new ConcurrentHashMap<E, Boolean>());
...
/* guaranteed to reflect the state of the set at read-time */
for (final E elem : safeSet) {
  ...
}


Answer 3:

我认为你最初的想法是合理的,并道韫的工作做得很好得到虫子。 除非你能找到的东西,做一切对你来说,这是更好地了解这些东西比希望一些神奇的类会为你做它。 魔法类可以使您的生活更轻松,减少失误的次数,但你要明白自己在做什么。

ConcurrentSkipListSet可能会为你在这里做一个更好的工作。 它可以摆脱所有的多线程问题。

然而,它比一个HashSet慢(一般 - HashSets和SkipLists /树难以比拟的)。 如果你正在做大量的阅读,每写,你得会更快。 更重要的是,如果你更新一次多个条目,你读能看到不一致的结果。 如果你希望,每当有一个条目的存在条目B,反之亦然,跳跃列表可以给你一个没有其他。

以您目前的解决方案,以飨读者,地图的内容总是内部一致。 读可以肯定的每一个B.可以肯定的是,一个A size()方法给出了将被迭代器返回元素的准确数目。 两个迭代都会以相同的顺序返回相同的元素。

换句话说,allUpdatesGoThroughHere和ConcurrentSkipListSet是两个不同的问题两个很好的解决方案。



Answer 4:

您可以使用Collections.synchronizedSet方法? 从HashSet中的Javadoc http://docs.oracle.com/javase/6/docs/api/java/util/HashSet.html

Set s = Collections.synchronizedSet(new HashSet(...));


Answer 5:

Replace the synchronized by making global volatile and you'll be alright as far as the copy-on-write goes.

Although the assignment is atomic, in other threads it is not ordered with the writes to the object referenced. There needs to be a happens-before relationship which you get with a volatile or synchronising both reads and writes.

The problem of multiple updates happening at once is separate - use a single thread or whatever you want to do there.

If you used a synchronized for both reads and writes then it'd be correct but the performance may not be great with reads needing to hand-off. A ReadWriteLock may be appropriate, but you'd still have writes blocking reads.

Another approach to the publication issue is to use final field semantics to create an object that is (in theory) safe to be published unsafely.

Of course, there are also concurrent collections available.



文章来源: In Java can I depend on reference assignment being atomic to implement copy on write?