【发布时间】:2016-01-16 21:55:14
【问题描述】:
来自《在 Scala 中学习并发编程》一书:
然而,在当前版本的 Scala (2.11.1) 中,某些集合是 被认为是不可变的,例如 List 和 Vector,不能共享 同步。尽管他们的外部 API 不允许您 修改它们,它们包含非最终字段。
谁能用一个小例子来证明这一点?这仍然适用于 2.11.7 吗?
【问题讨论】:
标签: scala concurrency
来自《在 Scala 中学习并发编程》一书:
然而,在当前版本的 Scala (2.11.1) 中,某些集合是 被认为是不可变的,例如 List 和 Vector,不能共享 同步。尽管他们的外部 API 不允许您 修改它们,它们包含非最终字段。
谁能用一个小例子来证明这一点?这仍然适用于 2.11.7 吗?
【问题讨论】:
标签: scala concurrency
从另一个线程查看时在一个线程中所做的更改的行为由Java Memory Model 控制。特别是,当涉及到构建一个集合,然后将构建的并且现在不可变的集合传递给另一个线程时,这些规则非常弱。 JMM 不保证其他线程不会看到未完全构建集合的早期视图!
由于synchronized 块强制执行排序,因此如果在每个操作中使用它们,它们可用于获得一致的视图。
但在实践中,这很少是真正需要的。在 CPU 端,通常有一个 memory barrier 操作可用于强制内存一致性(即,如果您写入列表的尾部,然后通过内存屏障,则没有其他线程可以看到尾部未设置)。在实践中,JVM 通常必须通过使用内存屏障来实现同步。因此,人们可能希望您可以在 synchronzied 块中传递创建的列表,相信会发出内存屏障,之后一切都会好起来的。
不幸的是,JMM 并不要求它以这种方式实现(并且您不能假设对象创建的类似内存屏障的行为实际上是一个完整的内存屏障,适用于该线程中的所有内容而不是简单的该对象的最终字段),这就是为什么推荐是它的原因,以及为什么它在库中没有固定(无论如何)。
对于它的价值,在 x86 架构上,如果您在 synchronized 块中传递不可变对象,我从未观察到问题。如果您尝试使用 CAS(例如,通过使用 java.util.concurrent.atomic 类),我发现了一些问题。
【讨论】:
java.util.concurrent.atomic.AtomicReference应该是安全的,因为原子访问和更新的记忆效应通常遵循易失性的规则[docs](原子访问和更新的记忆效应通常遵循挥发物的规则),对吧?
AtomicReference 内部的失败示例,但我不确定不会有。
作为 Rex Kerr 出色回答的补充:
应该注意的是,多线程上下文中不可变集合的最常见用例不受此问题的影响。这可能会影响您的唯一情况是当您做了一些您一开始可能不应该做的事情。
例如你有一个变量var x: Vector[Int],你从一个线程A写入并从另一个线程B读取。
如果您将x 标记为@volatile,则不会有问题,因为the volatile write introduces a memory barrier。因此,您将永远能够观察到处于不一致状态的 Vector。在读写时使用synchronized { }块或使用java.util.concurrent.atomic.AtomicReference时也是如此。
如果您不将x 标记为@volatile,您可能会观察到向量处于不一致状态(不仅仅是错误的元素,而是内部不一致! )。但在这种情况下,您的代码可能一开始就被破坏了。当您将在 B 中看到 A 的变化时,它完全未定义。
你可能会看到他们
取决于您运行的架构、月相等。正如 Viktor Klang 所说:"Unsafe publication is unsafe..."
请注意,如果您使用更高级别的并发框架(例如 akka Actor),还可以保证消息的接收者无法看到处于不一致状态的不可变集合。
【讨论】: