【问题标题】:Are static variables shared between threads?线程之间是否共享静态变量?
【发布时间】:2011-06-23 12:34:24
【问题描述】:

我在高级 Java 线程课程中的老师说了一些我不确定的话。

他表示以下代码不一定会更新ready 变量。据他介绍,两个线程不一定共享静态变量,特别是在每个线程(主线程与ReaderThread)都在自己的处理器上运行并且因此不共享相同的寄存器/缓存/等的情况下一个 CPU 不会更新另一个。

本质上,他说ready可能会在主线程中更新,但不会在ReaderThread中更新,因此ReaderThread会无限循环。

他还声称该程序可以打印042。我了解如何打印42,但不了解0。他提到当number 变量设置为默认值时会出现这种情况。

我想也许不能保证在线程之间更新静态变量,但这让我觉得 Java 很奇怪。使ready volatile 可以解决这个问题吗?

他展示了这段代码:

public class NoVisibility {  
    private static boolean ready;  
    private static int number;  
    private static class ReaderThread extends Thread {   
        public void run() {  
            while (!ready)   Thread.yield();  
            System.out.println(number);  
        }  
    }  
    public static void main(String[] args) {  
        new ReaderThread().start();  
        number = 42;  
        ready = true;  
    }  
}

【问题讨论】:

  • 非局部变量的可见性不取决于它们是静态变量、对象字段还是数组元素,它们都有相同的考虑。 (存在数组元素不能变为 volatile 的问题。)
  • 问问你的老师他认为什么样的建筑可以看到“0”。然而,理论上他是对的。
  • @bestsss 提出这样的问题会向老师表明他错过了他所说的全部内容。关键是有能力的程序员了解什么是有保证的,什么不是保证的,并且不要依赖没有保证的东西,至少在不准确理解什么是不保证的以及为什么不保证的情况下是这样。
  • 它们在同一个类加载器加载的所有东西之间共享。包括线程。
  • 你的老师(和公认的答案)是 100% 正确的,但我会提到它很少发生——这种问题会隐藏多年,只有在它会出现时才会出现是最有害的。即使是试图暴露问题的简短测试也倾向于表现得好像一切都很好(可能是因为他们没有时间让 JVM 进行太多优化),所以这是一个非常值得关注的问题。

标签: java multithreading concurrency static memory-visibility


【解决方案1】:

就可见性而言,静态变量并没有什么特别之处。如果它们可以访问,任何线程都可以访问它们,因此您更有可能看到并发问题,因为它们更容易暴露。

JVM 的内存模型存在可见性问题。 Here's an article talking about the memory model and how writes become visible to threads。您不能指望一个线程及时对其他线程可见的更改(实际上 JVM 没有义务在任何时间范围内使这些更改对您可见),除非您建立happens-before relationship

这是来自该链接的引用(由 Jed Wesley-Smith 在评论中提供):

Java 语言规范的第 17 章定义了内存操作(例如共享变量的读取和写入)的发生前关系。只有当写操作发生在读操作之前,一个线程写的结果才能保证对另一个线程的读可见。 synchronized 和 volatile 构造,以及 Thread.start() 和 Thread.join() 方法,可以形成happens-before 关系。特别是:

  • 线程中的每个动作都发生在该线程中按程序顺序稍后出现的每个动作之前。

  • 监视器的解锁(同步块或方法退出)发生在同一监视器的每个后续锁定(同步块或方法入口)之前。并且由于happens-before关系是可传递的,因此线程在解锁之前的所有动作都发生在任何线程锁定该监视器之后的所有动作之前。

  • 对 volatile 字段的写入发生在对同一字段的每次后续读取之前。 volatile 字段的写入和读取具有与进入和退出监视器类似的内存一致性效果,但不需要互斥锁定。

  • 在线程上启动的调用发生在已启动线程中的任何操作之前。

  • 线程中的所有操作都发生在任何其他线程从该线程的连接成功返回之前。

【讨论】:

  • 在实践中,“及时”和“永远”是同义词。上面的代码很有可能永远不会终止。
  • 另外,这展示了另一种反模式。不要使用 volatile 来保护多个共享状态。在这里,number 和 ready 是两种状态,要一致地更新/读取它们,您需要实际同步。
  • 关于最终变得可见的部分是错误的。如果没有任何明确的发生前关系,则无法保证任何写入将永远被另一个线程看到,因为 JIT 完全有权将读取强加到寄存器中,然后您将永远不会查看任何更新。任何最终的负载都是运气,不应依赖。
  • "除非您使用 volatile 关键字或同步。"应阅读“除非作者和读者之间存在相关的发生前关系”和此链接:download.oracle.com/javase/6/docs/api/java/util/concurrent/…
  • @bestsss 很好发现。不幸的是,ThreadGroup 在很多方面都被破坏了。
【解决方案2】:

当你初始化静态原始类型变量时,java默认为静态变量赋值

public static int i ;

当你像这样定义变量时,默认值 i = 0; 这就是为什么有可能让你得到 0。 然后主线程将 boolean ready 的值更新为 true。由于 ready 是一个静态变量,主线程和其他线程引用相同的内存地址,所以 ready 变量会发生变化。所以辅助线程从while循环中退出并打印值。 打印时 number 的值初始化值为 0。如果线程进程在主线程更新 number 变量之前已经通过了 while 循环。那么就有可能打印 0

【讨论】:

    【解决方案3】:

    @dontocsata 你可以回到你的老师那里,让他学一点:)

    很少有来自现实世界的笔记,无论你看到什么或被告知什么。 请注意,下面的文字是关于这个特殊情况的,按显示的确切顺序。

    以下 2 个变量将驻留在几乎任何已知架构下的同一缓存行上。

    private static boolean ready;  
    private static int number;  
    

    Thread.exit(主线程)保证会退出,exit 保证会导致内存栅栏,因为线程组线程删除(以及许多其他问题)。 (这是一个同步调用,我看不到没有同步部分可以实现的单一方法,因为如果没有留下任何守护线程等,ThreadGroup 也必须终止。

    已启动的线程ReaderThread 将使进程保持活动状态,因为它不是守护进程! 因此readynumber 将一起刷新(如果发生上下文切换,则为之前的数字),并且在这种情况下没有真正的重新排序理由,至少我想不出一个。 除了42,您将需要一些真正奇怪的东西才能看到任何东西。我再次假设两个静态变量将位于同一缓存行中。我只是无法想象一个 4 字节长的缓存行或一个不会在连续区域(缓存行)中分配它们的 JVM。

    【讨论】:

    • @bestsss 虽然今天这​​一切都是正确的,但它的真实性依赖于当前的 JVM 实现和硬件架构,而不是程序的语义。这意味着程序仍然被破坏,即使它可能工作。很容易找到这个例子的一个简单的变体,它实际上以指定的方式失败。
    • 我确实说过它不符合规范,但是作为老师至少找到一个合适的例子,它实际上可能在某些商品架构上失败,所以这个例子是真实的。
    • 旁注:提醒我 java.util.concurrent.Exchanger 如何通过添加 15 个额外的 long 来避免 fake isolation(需要一个链接,我知道)来增加缓存行。
    • @Bestsss:您的问题的简单答案就是:“规范和文档的代码,而不是特定系统或实现的副作用”。这在旨在与底层硬件无关的虚拟机平台中尤其重要。
    • @Bestsss:老师的观点是(a)代码在您测试时可能会很好地工作,并且(b)代码被破坏,因为它取决于硬件操作而不是规范的保证.重点是看起来还行,其实不行。
    【解决方案4】:

    他说的是可见性,不要太字面意思。

    静态变量确实在线程之间共享,但是在一个线程中所做的更改可能不会立即对另一个线程可见,使得变量看起来像是有两个副本。

    这篇文章所呈现的观点与他呈现信息的方式是一致的:

    首先,您必须对 Java 内存模型有所了解。多年来,我一直在努力简单而准确地解释它。到今天为止,我能想到的最好的描述方式就是如果你这样想:

    • Java 中的每个线程都发生在一个单独的内存空间中(这显然是不真实的,所以请耐心等待)。

    • 您需要使用特殊机制来保证这些线程之间发生通信,就像在消息传递系统上一样。

    • 在一个线程中发生的内存写入可能会“泄漏”并被另一个线程看到,但这绝不是保证。如果没有明确的通信,您无法保证其他线程可以看到哪些写入,甚至无法保证看到它们的顺序。

    ...

    但同样,这只是一个思考线程和易失性的心智模型,而不是 JVM 的工作原理。

    【讨论】:

      【解决方案5】:

      当然,它们是“共享的”,因为它们都引用同一个变量,但它们不一定看到彼此的更新。这适用于任何变量,而不仅仅是静态变量。

      从理论上讲,另一个线程进行的写入可能会以不同的顺序出现,除非变量声明为 volatile 或写入显式同步。

      【讨论】:

        【解决方案6】:

        基本上是这样,但实际上问题更复杂。共享数据的可见性不仅会受到 CPU 缓存的影响,还会受到指令的乱序执行的影响。

        因此,Java 定义了一个Memory Model,它说明在哪些情况下线程可以看到共享数据的一致状态。

        在您的特定情况下,添加 volatile 可以保证可见性。

        【讨论】:

          【解决方案7】:

          在单个类加载器中,静态字段始终是共享的。要将数据显式限定为线程,您需要使用ThreadLocal 之类的工具。

          【讨论】:

            猜你喜欢
            • 2015-06-17
            • 1970-01-01
            • 1970-01-01
            • 2013-06-20
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2014-08-07
            相关资源
            最近更新 更多