同步跨线程共享但不同时访问的对象

假设我有一个包含字段data的共享对象。 多个线程将共享对此对象的引用以访问该字段。 但是,线程永远不会同时访问对象。 我是否需要将data声明为volatile?

这种情况如下:

  • Counter定义唯一的字段value和一个方法increment
  • 一个线程递增计数器,然后产生另一个递增计数器的线程,等等。

鉴于该程序的逻辑,没有并行访问计数器。 然而,计数器在多个线程中共享。 柜台必须是不稳定的吗?

这种情况的另一种变体是当多个线程操纵一个普通数据的对象X,但是通过依赖于并发控制( waitnotifysynchronize )的另一个对象Y来交替它们的时间执行(以便从不同时访问X)。 对象X的字段是否应该是易变的?

我们强烈建议学习Java内存模型的整个JLS章节 – 实际上是强制性的 – 适用于使用Java进行并发的任何人。 具体而言,您的案例包含在JLS,17.4.4中:

“启动线程的动作与它启动的线程中的第一个动作同步。”

这意味着对于您的第一个场景,您不需要volatile 。 但是,无论如何都要保持对代码的未来更改是健全的。 你应该有充分的理由不要使用volatile ,只有在读取率非常高的情况下(至少每秒数百万)。

Java内存模型和字节码重新排序不保证后续线程将看到计数器的递增值。 因此,如果您使用单线程 – 您不需要对volatile进行任何操作,但如果有多个线程可以从变量中读取内容 – 您需要确保使用volatile或者同步/锁定来查看对另一个线程的更改。

Thread.start方法强加了屏障,因此可以确保可见性 – 并且可能发生您不需要那些易变的东西。 但无论如何我还会添加它。

关于问题的第二部分:如果不在变量X上使用volatile,则给定线程可能总是使用变量值的本地缓存版本。 您将变量Y用作锁定将非常有效,可以确保两个线程不会同时写入X,但无法保证其中一个线程不会查看过时数据。

来自JLS:“写入易失性变量v与任何线程的v的所有后续读取同步”。 我读这个的方式是规范不保证除了v之外对其他变量的读取。

你只用柜台讲述了故事的一部分。 计数器的递增部分似乎很好 – 正如Marko指出的那样,Thread.start上有一个HB边缘。 但谁在读这个柜台? 如果它是除了这些衍生线程之外的任何人,并且你完全关心看到最新的值,那么该字段需要是不稳定的。 如果计数器是long (或double ),即使你不关心陈旧值,你也需要它是易变的,因为否则你可能会撕裂。

只有在线程之间建立了先发生关系时,才能保证线程的突变对其他线程可见。 建立关系后,所有先前的突变都变得可见。

如果另一个对象正确地同步对它的访问,那么在隔离时未正确同步的对象可以安全使用(请参阅Java Concurrency in Practice中的piggibacking )。

在问题中描述的两种情况下,我认为不需要同步:

  • Thread.start建立了之前发生的关系,因此来自先前线程的所有突变都是可见的
  • 对象X的访问由对象Y同步,这将建立发生在之前的关系并使对X的更改可见(我在博客文章中扩展了一点)。

如果您知道永远不会同时访问对象X,则可能存在间接同步对X的访问的对象Y,因此它很好。 我看到的唯一不安全的情况是线程是否按时传递(例如使用Thread.sleep或循环直到一段时间已经消失)以保证相互排斥:在这种情况下,没有发生在之前建立的关系。