【问题标题】:Do I need the volatile keyword? (Java)我需要 volatile 关键字吗? (爪哇)
【发布时间】:2016-06-05 20:18:03
【问题描述】:

如果多个线程同时读取一个字段,我是否只需要将其标记为 volatile?

如果线程A改变了一个字段的值,线程B在保证线程A完成后评估它呢?

在我的场景中,是否强制执行发生前的关系(没有 volatile 关键字)?

【问题讨论】:

  • 如果线程B执行了A.join(),那么读取变量就是线程A写入的值,变量不需要是volatile的。
  • 从 B 的角度来看,如何保证线程 A 已经完成?
  • 我正在考虑的更详细的例子:主线程有一个 ExecutorService 提交 Runnable A。主线程检查 Runnable A 上在异步运行时更改的字段。我们通过在返回的 Future 上调用 get() 来保证线程已完成。
  • 在没有 volatile 的情况下发生之前发生的最直接的方法当然是使用同步

标签: java multithreading volatile


【解决方案1】:

您需要 volatile 关键字或其他一些同步机制来强制执行“发生在之前”关系,以保证变量在线程中的可见性,而不是写入它的线程。如果没有这种同步,所有事情都被视为“同时”发生,即使不是按挂钟时间。

在您的特定示例中,在没有同步的情况下可能发生的一件事是线程 A 写入的值永远不会从缓存刷新到主内存,线程 B 在另一个处理器上执行并且永远不会看到线程 A 写入的值。

当您处理线程时,挂钟时间毫无意义。如果您希望数据在线程之间正确传递,则必须正确同步。正确同步没有捷径,以后不会让您头疼。

就您最初的问题而言,可以通过使用volatile 关键字、使用synchronized 块或让正在读取变量join() 值的线程来实现正确同步的一些方法写入变量的线程。

编辑:针对您的评论,Future 具有内部同步,因此在 Future 上调用 get() 会在调用返回时建立“发生在之前”的关系,因此也可以实现正确同步。

【讨论】:

  • 只是一个重要的说明:您不需要线程 B 在另一个处理器上运行来遇到内存可见性问题(换句话说,这也可能发生在单核 CPU 上)。一个实际原因是 JIT/Hotspot 优化导致代码将某些值保留在 CPU 寄存器中,而无需读取或写入内存。您需要适当的 happens-before 关系来间接告诉优化器限制。
  • @Holger 同意。我举了一个简单的例子,因为有时不熟悉多线程的人会怀疑为什么在涉及宏观经过时间时需要同步。
  • 这是否意味着一旦线程退出,同步块会导致写入其中的所有内容都写入主内存?
  • @ArtemNovikov 如果两个同步块在同一个对象上同步,则写入同步块的信息将对稍后在另一个同步块中运行的代码可见。这可能涉及也可能不涉及对主存储器的写入。我的回答描述了同步如何出错的一个例子,但这不是唯一的方法。
  • 请参考规范?
【解决方案2】:

不,你不需要volatile...

是否强制执行发生前的关系(没有 volatile 关键字)?

...但是您的代码需要做一些事情来建立“之前发生”。

如果(并且如果)您的代码执行“Java 语言规范”(JLS)所说的将建立“发生——之前。”

如果线程A改变了一个字段的值,线程B在保证线程A完成后评估它呢?

取决于您所说的“保证”是什么意思。如果“保证”的意思是“之前发生”,那么您的代码将按预期工作。

可以保证线程 B 调用threadA.join() 的一种方法。 JLS 保证如果线程 B 调用 threadA.join(),那么线程 A 所做的一切都必须“发生在”join() 调用返回之前。

如果线程 B 仅在加入线程 A 后访问它们,则您不需要任何共享变量为 volatile

【讨论】:

    【解决方案3】:

    您可以选择一种可用的选项来实现相同的目的。

    1. 您可以使用volatile 强制所有线程从主内存中获取变量的最新值。
    2. 您可以使用synchronization 来保护关键数据
    3. 您可以使用Lock API
    4. 你可以使用Atomic变量

    请参阅此文档page 了解高级并发构造。

    查看相关的 SE 问题:

    Avoid synchronized(this) in Java?

    What is the difference between atomic / volatile / synchronized?

    【讨论】: