我的老师在一个关于线程的高级Java课上说了一些我不确定的事情。
他说以下代码不一定会更新ready
变量。根据他的说法,两个线程不一定共享静态变量,特别是在每个线程(主线程与ReaderThread
)在其自己的处理器上运行并因此不共享相同的寄存器/缓存的情况下/ etc和一个CPU不会更新另一个。
基本上,他说有可能ready
在主线程中更新,但不在ReaderThread
中,因此ReaderThread
将无限循环。
他还声称该程序可以打印0
或42
。我了解如何打印42
,而不是0
。他提到当number
变量设置为默认值时会出现这种情况。
我想也许并不能保证在线程之间更新静态变量,但这对我来说非常奇怪。让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;
}
}
答案 0 :(得分:73)
在可见性方面,静态变量没有什么特别之处。如果它们是可访问的,任何线程都可以访问它们,因此您更有可能看到并发性问题,因为它们更容易暴露。
JVM的内存模型存在可见性问题。 Here's an article talking about the memory model and how writes become visible to threads。您不能指望一个线程及时变得对其他线程可见的更改(实际上JVM没有义务在任何时间框架中对您显示这些更改),除非您建立{{3} }。
以下是该链接的引用(由Jed Wesley-Smith发表的评论中提供):
Java语言规范的第17章定义了内存操作的先发生关系,例如共享变量的读写。只有在读取操作之前发生写入操作时,一个线程的写入结果才能保证对另一个线程的读取可见。 synchronized和volatile构造以及Thread.start()和Thread.join()方法可以形成先发生关系。特别是:
线程中的每个动作都发生在该程序订单后面的该线程中的每个动作之前。
监视器的解锁(同步块或方法退出)发生在同一监视器的每个后续锁定(同步块或方法条目)之前。并且因为发生在之前的关系是可传递的,所以在解锁之前线程的所有动作都发生在任何线程锁定该监视器之后的所有动作之前。
在每次后续读取同一字段之前,会发生对易失性字段的写入。易失性字段的写入和读取具有与进入和退出监视器类似的内存一致性效果,但不需要互斥锁定。
在启动线程中的任何操作之前发生对线程启动的调用。
线程中的所有操作都发生在任何其他线程从该线程上的连接成功返回之前。
答案 1 :(得分:34)
他在谈论 知名度 ,而不是太过于字面意思。
静态变量确实在线程之间共享,但是在一个线程中所做的更改可能不会立即对另一个线程可见,使得看起来好像有两个变量副本。
本文提供的观点与他提供信息的方式一致:
首先,您必须了解Java内存模型的一些内容。多年来我一直在努力解释它。截至今天,我能想到的最好的方式就是用这种方式想象:
Java中的每个线程都在一个单独的内存空间中发生(这显然是不真实的,所以请耐心等待。)
您需要使用特殊机制来保证在这些线程之间进行通信,就像在消息传递系统上一样。
在一个线程中发生的内存写入可以“泄漏”并被另一个线程看到,但这绝不是保证。如果没有明确的沟通,您无法保证其他线程可以看到哪些写入,甚至不能保证看到它们的顺序。
...
但同样,这只是思考线程和易失性的心理模型,而不是字面上JVM的工作方式。
答案 2 :(得分:12)
基本上这是真的,但实际上问题更复杂。共享数据的可见性不仅会受到CPU缓存的影响,还会受到乱序执行指令的影响。
因此,Java定义了Memory Model,表明线程可以在哪种情况下看到共享数据的一致状态。
在您的特定情况下,添加volatile
可确保可见性。
答案 3 :(得分:7)
当然,它们是“共享的”,因为它们都引用相同的变量,但它们不一定会看到彼此的更新。这适用于任何变量,而不仅仅是静态变量。
理论上,除非变量声明为volatile
或写入是明确同步的,否则另一个线程的写入可能看起来是不同的顺序。
答案 4 :(得分:4)
在单个类加载器中,静态字段始终是共享的。要将数据明确地定位到线程,您需要使用ThreadLocal
等工具。
答案 5 :(得分:2)
初始化静态基元类型变量时,java default为静态变量赋值
public static int i ;
当你定义这样的变量时,默认值为i = 0; 这就是为什么有可能让你0。 然后主线程将boolean ready的值更新为true。因为ready是一个静态变量,主线程和另一个线程引用相同的内存地址所以就绪变量。所以辅助线程从while循环和打印值中退出。 当打印值初始化值为0时,如果线程进程已经在主线程更新号变量之前循环。然后有可能打印0
答案 6 :(得分:-2)
@dontocsata 你可以回到你的老师那里给他上学一点:)
来自现实世界的一些笔记,无论你看到或被告知什么。 请注意,以下单词是按照所示的确切顺序对此特定情况进行的。以下2个变量将驻留在几乎任何已知架构下的同一缓存行中。
private static boolean ready;
private static int number;
由于线程组线程的移除(以及许多其他问题), Thread.exit
(主线程)保证退出并且exit
保证会导致内存栅栏。 (这是一个同步调用,我认为没有一种方法可以用同步部分实现,因为如果没有留下守护进程线程,ThreadGroup也必须终止,等等。)
启动的线程ReaderThread
将使进程保持活动状态,因为它不是守护进程!
因此ready
和number
将被刷新在一起(或者如果发生上下文切换之前的数字),并且在这种情况下没有真正的重新排序原因,至少我甚至不能想到一个。
除42
之外,你需要一些看起来很奇怪的东西。我再次假设两个静态变量都在同一个缓存行中。我无法想象一个4字节长的缓存行或一个不会在连续区域(缓存行)中分配它们的JVM。