96

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

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

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

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

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

他展示了这段代码:

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

7 回答 7

77

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

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

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

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

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

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

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

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

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

于 2011-02-08T15:31:00.110 回答
37

他说的是能见度,不要太从字面上理解。

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

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

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

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

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

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

...

螺纹模型

但同样,这只是一个思考线程和易失性的心智模型,而不是真正意义上的 JVM 是如何工作的。

于 2011-02-08T15:35:30.247 回答
12

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

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

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

于 2011-02-08T15:33:26.980 回答
8

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

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

于 2011-02-08T15:34:30.470 回答
5

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

于 2011-02-08T15:27:40.543 回答
3

当您初始化静态原始类型变量时,java默认为静态变量分配一个值

public static int i ;

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

于 2018-04-19T14:31:33.153 回答
-2

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

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

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

private static boolean ready;  
private static int number;  

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

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

于 2011-02-11T00:06:31.573 回答