线程之间共享静态变量吗?


95

我的高级Java课堂上有关线程的老师说了一些我不确定的东西。

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

从本质上讲,他说有可能ready在主线程中进行更新,而不是在中进行更新ReaderThread,因此ReaderThread将无限循环。

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

我认为也许不能保证在线程之间更新静态变量,但是这对Java来说很奇怪。使ready挥发物能纠正这个问题吗?

他显示了以下代码:

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;  
    }  
}

非局部变量的可见性并不取决于它们是静态变量,对象字段还是数组元素,它们都有相同的考虑因素。(存在数组元素不能变得易变的问题。)
PaŭloEbermann

1
问你的老师他认为什么样的建筑,可能会看到“ 0”。但是,从理论上讲,他是对的。
bestsss 2011年

4
@bestsss提出这样的问题会向老师表明,他错过了他所说的全部内容。关键是有能力的程序员理解什么是保证的,什么不是保证,并且不依赖那些不能保证的事物,至少在没有确切地理解什么不能保证以及为什么的情况下。
David Schwartz

它们在同一类加载器加载的所有东西之间共享。包括线程。
罗恩侯爵

您的老师(和接受的答案)是100%正确的,但是我会提到这种情况很少发生-这种问题会隐藏多年,并且只会在危害最大的时候表现出来。即使是试图揭示问题的简短测试也可能看起来好像一切都很好(可能是因为它们没有时间让JVM进行很多优化),所以这是一个非常好的问题。
比尔K

Answers:


75

关于可见性,静态变量没有什么特别的。如果可以访问它们,那么任何线程都可以使用它们,因此您很可能会发现并发问题,因为它们更容易暴露。

JVM的内存模型强加了可见性问题。这是一篇有关内存模型以及线程如何看到写入的文章。您不能指望一个线程能够及时对其他线程可见的更改(实际上,JVM没有义务在任何时间范围内完全使这些更改对您可见),除非您建立事前发生的关系

这是该链接的引文(Jed Wesley-Smith的评论中提供):

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

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

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

  • 每次对同一字段进行后续读取之前,都会写入易失字段。易失性字段的写入和读取与进入和退出监视器具有相似的内存一致性效果,但是不需要互斥锁定。

  • 在启动线程中的任何操作之前,都会发生对启动线程的调用。

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


3
实际上,“及时”和“永远”是同义词。上面的代码很可能永远不会终止。
TREE

4
此外,这还展示了另一种反模式。不要使用volatile保护多个共享状态。在这里,number和ready是两种状态,要始终如一地更新/读取它们,您需要实际的同步。
TREE

5
关于最终变得可见的部分是错误的。没有任何明确的之前发生关系没有保证任何写操作将永远被另一个线程可以看出,随着JIT是相当之内的权利强加于读取到寄存器中,那么你永远也看不到任何更新。任何最终的负载都是运气好的,因此不应依赖。
Jed Wesley-Smith'2

2
“除非您使用volatile关键字或进行同步。” 应改为“除非有相关的之前发生的作家和读者之间的关系”,并点击此链接:download.oracle.com/javase/6/docs/api/java/util/concurrent/...
杰德韦斯利·史密斯

2
@bestsss被发现。不幸的是,ThreadGroup被破坏的方式很多。
Jed Wesley-Smith'2

37

他在谈论可见度,而不是从字面上看。

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

本文提出的观点与他介绍信息的方式一致:

首先,您必须了解一些有关Java内存模型的知识。多年来,我一直在努力地简要解释它。到目前为止,我能想到的最好的描述方法是,如果您这样想:

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

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

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

...

螺纹模型

再次重申,这只是思考线程和易失性的思维模型,而不是JVM的工作原理。


12

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

因此,Java定义了一个内存模型,该声明线程在这种情况下可以看到共享数据的一致状态。

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


8

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

从理论上讲,除非声明了变量volatile或显式同步了写入操作,否则另一个线程进行的写入似乎以不同的顺序进行。


4

在单个类加载器内,静态字段总是共享的。要将数据显式作用域范围内,您需要使用类似的功能ThreadLocal


2

初始化静态基本类型变量时,java默认会为静态变量分配一个值

public static int i ;

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


-2

@dontocsata,您可以回到您的老师那里再学一点:)

很少有来自现实世界的笔记,无论您看到或被告知什么。请注意,以下词语与该特定情况有关,其确切顺序如下所示。

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

private static boolean ready;  
private static int number;  

Thread.exitexit由于线程组线程删除(以及许多其他问题),(主线程)被保证退出并保证引起内存隔离。(这是一个同步调用,我看不到没有同步部分的任何一种实现方式,因为如果没有后台驻留程序线程,ThreadGroup也必须终止,等等)。

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


3
@bestsss虽然今天是正确的,但它的真实性依赖于当前的JVM实现和硬件体系结构,而不是程序的语义。这意味着该程序即使可以运行也仍然被破坏。很容易找到这个示例的琐碎变体,但实际上确实以指定的方式失败了。
Jed Wesley-Smith'2

1
我确实说过,它没有遵循规范,但是作为一名老师,至少找到了一个在某些商品体系结构上实际上可能会失败的合适示例,所以该示例是真实的。
bestsss 2011年

6
我看到的关于编写线程安全代码的最坏建议可能是。
劳伦斯·多尔

4
@Bestsss:您的问题的简单答案就是:“规范和文档的代码,而不是特定系统或实现的副作用”。这在设计为不可知底层硬件的虚拟机平台中尤其重要。
劳伦斯·多尔

1
@Bestsss:老师的观点是(a)在测试时该代码可能会正常工作,并且(b)该代码已损坏,因为它取决于硬件操作,而不是规范的保证。关键是它看起来还可以,但不是很好。
劳伦斯·多尔
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.