英文:
How volatile keyword works?
问题
我正在阅读关于 volatile
关键字的内容。阅读了关于 volatile
关键字的内容后,我浏览了下面的示例以获得更多理解。
public class TaskRunner {
private static int number;
private static volatile boolean ready;
private static class Reader extends Thread {
@Override
public void run() {
while (!ready) {
Thread.yield();
}
System.out.println(number);
}
}
public static void main(String[] args) {
new Reader().start();
number = 42;
ready = true;
}
}
我所理解的是,在Java应用程序中,多个线程可以在任何时间访问共享数据结构。有些线程第一次对其进行写入,有些更新,有些读取等等。
因此,在这些情况下,每个线程都只从主内存访问共享数据结构的
值。但有时候线程操作的值在共享数据结构
上仍然保留在其缓存中,直到操作系统将其放入主内存为止。因此,在此期间,如果任何其他线程访问共享数据结构,将无法获取由上一个线程更新并仍然保留在其缓存中的更新值。
volatile
被用于在共享数据结构的值更改后,应该先将其移到主内存,然后其他任何线程才能访问它。这是正确的理解吗?
什么情况下线程在使用了 volatile
后仍然无法获取更新后的值?
英文:
I'm reading about volatile
keyword. After reading about volatile
keyword, Going through below example for more understanding.
public class TaskRunner {
private static int number;
private static boolean ready;
private static class Reader extends Thread {
@Override
public void run() {
while (!ready) {
Thread.yield();
}
System.out.println(number);
}
}
public static void main(String[] args) {
new Reader().start();
number = 42;
ready = true;
}
}
What I've understood that, In Java application multiple threads can access shared data structure at any point of time. Some writes to it first time, some updates and some reads them and so on.
So while these happenings, every thread access the shared data structure's
value from main memory only. But some times thread's operated value on shared data structure
remains in its cache until the OS dont put it in main memory. So in that duration if any other thread access the shared data structure, will not get the updated value, which is updated by last thread and still in its cache.
Volatile
is used, once shared data structure value is changed, should be moved to main memory first before it get accessed by any other thread. Is it correct understanding ?
What's scenario where thread still not getting updated value even after using volatile
?
答案1
得分: 1
但有时线程对共享数据结构的操作值会保留在其缓存中,直到操作系统将其放入主内存。因此,在这段时间内,如果任何其他线程访问共享数据结构,将无法获取由上一个线程更新的更新值,该值仍然保留在其缓存中。
这与操作系统无关。JVM使用一条CPU指令来重置CPU缓存。老实说,这种说法也是不正确的,因为Java内存模型对于此类指令没有明确说明。这是实现
volatile
行为的一种方式。
英文:
> But some times thread's operated value on shared data structure remains in its cache until the OS dont put it in main memory. So in that duration if any other thread access the shared data structure, will not get the updated value, which is updated by last thread and still in its cache.
It is not the OS. There is a CPU instruction that is used by JVM to reset CPU cache. Honestly speaking, this claim is incorrect too, because Java Memory Model tells nothing about such instructions. This is one of the ways to implement volatile
behaviour.
答案2
得分: 1
Java 是相当高级的:作为一种语言,它并没有针对任何特定的 CPU 设计。此外,Java 编译成字节码,这是一个中间产物:Java 既不提供,也不具备让你编写低级 CPU 架构特定操作的目标。
然而,缓存是一个与低级 CPU 架构相关的概念。当然,现代的每个 CPU 几乎都有缓存,但谁知道在 20 年后会发生什么?
因此,将 volatile 解释为其对 CPU 缓存的影响是在跳过了一些步骤。
volatile
对你的 Java 代码产生影响。就我所了解,大多数虚拟机目前是通过向 CPU 发送有关刷新缓存的指令来实现这种影响的。
最好在 Java 代码层面处理 volatile,而不是在“大多数虚拟机像这样实现”的层面处理,毕竟这可能会发生变化。
Java 的设置基本上是这样的:
如果在 Java 的任何地方没有建立任何两行代码之间的前序关系,那么你应该假设 Java 就像薛定谔的猫:每个线程既具有并且又不具有整个虚拟机中加载的每个对象上的每个字段的本地缓存副本,无论何时你写入或读取任何内容,宇宙都会抛一枚硬币,用它来确定你是否获得副本,然后总是会掷硬币来干扰你。在你自己的机器上进行测试时,硬币会掷硬币使测试通过。在周末紧张的生产中,当数百万美元岌岌可危时,硬币会掷硬币使你的代码失败。
唯一的解决办法是确保你的代码不依赖于硬币的抛掷。
做到这一点的方法是使用前序关系规则,你可以在 Java 内存模型中进行审查。
volatile 是添加前序关系的一种方式。
在上面的代码中,如果没有使用 volatile,读取线程可能始终使用其本地的 ready
副本,因此即使从主线程设置为 true 后已经过去了很多小时,读取线程也永远不会准备好。实际上,这是不太可能的,但 Java 内存模型表示虚拟机可以在这里掷硬币:它可以让你的读取线程几乎立即继续,也可以阻塞它一个小时,甚至可以永远阻塞。所有这些都是合法的 - 这段代码是有问题的,它的行为取决于硬币的抛掷,这是不好的。
然而,一旦引入了 volatile,你就建立了前序关系,现在你保证了读取线程会继续。实际上,volatile 在标记变量时既禁用了对该变量的硬币抛掷,又在读取/写入重要时建立了前序关系:
如果 一个线程观察到 volatile 变量的更新值,那么 所有在任何线程的代码中早于更新该变量的代码之前运行的代码都与在读取更新的代码之后将要运行的代码具有前序关系。
因此,为了清楚起见:
在没有 任何 volatile 标记的情况下,虚拟机可以合法地让读取线程永远挂起。虚拟机也可以合法地让读取线程继续(使其观察到 ready
现在为 true
),尽管读取线程仍然看到 number
为 0(而不是 42),即使它通过了准备检查! - 但它不必这样做,虚拟机也允许读取线程永远不通过准备检查,或者通过准备检查并观察到 42。虚拟机可以以任何它想要的方式进行操作;无论对于当前的 CPU、架构和月相混合来说,哪种方式似乎更快。
有了 volatile,读取线程一定会较早地继续执行,一旦它这样做,它一定会观察到 42
。但是,如果你交换 ready = true;
和 number = 42;
,这种保证将不再成立。
英文:
Java is rather high-level: As a language it is not designed for any particular CPU design. Furthermore, java compiles to bytecode, which is an intermediate product: Java does not offer, nor does it have the goal of, letting you write low-level CPU-architecture-specific operations.
And yet, caches are a low-level CPU architecture specific concept. Sure, every modern CPU has them, pretty much, but who knows what happens in 20 years?
So putting volatile in terms of what it does to CPU caches is skipping some steps.
volatile
has an effect on your java code. That effect is currently implemented on most VMs I know of by sending the CPU some instructions about flushing caches.
It's better to deal with volatile at the java level itself, not at the 'well most VMs implement it like this' level - after all, that can change.
The way java is set up is essentially as follows:
If there are no comes-before relationships established between any 2 lines of code anywhere in java, then you should assume that java is like schroedinger's cat: Every thread both has and does not have a local cached copy of every field on every object loaded in the entire VM, and whenever you either write or read anything, the universe flips a coin, uses that to determine if you get the copy or not, and will always flip it to mess with you. During tests on your own machine, the coin flips to make the tests pass. During production on crunch weekend when millions of dollars are on the line, it flips to make your code fail.
The only way out is to ensure your code doesn't depend on the coin flip.
The way to do that is to use the comes-before rules, which you can review in the Java Memory Model.
volatile is one way to add them.
In the above code, without the volatile, the Reader thread may always use its local copy of ready
, and thus will NEVER be ready, even if it has been many hours since your main set ready
to true. In practice that's unlikely but the JMM says that a VM is allowed to coin flip here: It may have your Reader thread continue almost immediately, it may hold it up for an hour, it may hold it up forever. All legal - this code is broken, its behaviour depends on the coin flip which is bad.
Once you introduce volatile, though, you establish a comes-before relationship and now you're guaranteeing that Reader continues. Effectively, volatile both disables coinflips on the variable so marked and also established comes-before once reads/writes matter:
IF a thread observes an updated value in a volatile variable, THEN all lines that ran before whatever thread's code updated that variables have a comes-before relationship with all lines that will ran after the code in the thread that read the update.
So, to be clear:
Without any volatile marks here, it is legal for a VM to let Reader hang forever. It is also legal for a VM to let reader continue (to let it observe that ready
is now true
, whilst Reader STILL sees that number
is 0 (and not 42), even after it passed the ready check! - but it doesn't have to, it is also allowed for the VM to have reader never pass the ready check, or to have it pass ready check and observe 42. A VM is free to do it however it wants; whatever seems fastest for this particular mix of CPU, architecture and phase of the moon right now.
With volatile, reader WILL end up continuing sooner rather than later, and once it has done so, it will definitely observe 42
. But if you swap ready = true;
and number = 42;
that guarantee is no longer granted.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论