简体   繁体   English

了解 Java 易失性可见性

[英]Understanding Java volatile visibility

I'm reading about the Java volatile keyword and have confusion about its 'visibility'.我正在阅读有关 Java volatile关键字的信息,但对其“可见性”感到困惑。

A typical usage of volatile keyword is: volatile 关键字的典型用法是:

volatile boolean ready = false;
int value = 0;

void publisher() {
    value = 5;
    ready = true;
}

void subscriber() {
    while (!ready) {}
    System.out.println(value);
}

As explained by most tutorials, using volatile for ready makes sure that:正如大多数教程所解释的,使用 volatile for ready可确保:

  • change to ready on publisher thread is immediately visible to other threads (subscriber);在发布者线程上更改为ready对其他线程(订阅者)立即可见;
  • when ready 's change is visible to other thread, any variable update preceding to ready (here is value 's change) is also visible to other threads;ready的变化是对其他线程可见,前述的任何变量更新ready (这里是value的变化)也是其他线程可见的;

I understand the 2nd, because volatile variable prevents memory reordering by using memory barriers, so writes before volatile write cannot be reordered after it, and reads after volatile read cannot be reordered before it.我理解第二点,因为volatile变量通过使用内存屏障来防止内存重新排序,所以在 volatile write 之前的写入不能在它之后重新排序,并且在 volatile read 之后的读取不能在它之前重新排序。 This is how ready prevents printing value = 0 in the above demo.这就是ready防止上面演示中打印value = 0 的方式。

But I have confusion about the 1st guarantee, which is visibility of the volatile variable itself.但是我对第一个保证感到困惑,即 volatile 变量本身的可见性。 That sounds a very vague definition to me.对我来说,这听起来是一个非常模糊的定义。

In other words, my confusion is just on SINGLE variable's visibility, not multiple variables' reordering or something.换句话说,我的困惑仅在于 SINGLE 变量的可见性,而不是多个变量的重新排序或其他什么。 Let's simplify the above example:让我们简化上面的例子:

volatile boolean ready = false;

void publisher() {
    ready = true;
}

void subscriber() {
    while (!ready) {}
}

If ready is not defined volatile, is it possible that subscriber get stuck infinitely in the while loop?如果ready没有定义为 volatile,那么订阅者是否有可能无限地卡在 while 循环中? Why?为什么?

A few questions I want to ask:我想问几个问题:

  • What does 'immediately visible' mean? “立即可见”是什么意思? Write operation takes some time, so after how long can other threads see volatile's change?写操作需要一些时间,那么其他线程多久可以看到 volatile 的变化呢? Can a read in another thread that happens very shortly after the write starts but before the write finishes see the change?在写入开始后不久但在写入完成之前发生的另一个线程中的读取是否可以看到更改?
  • Visibility, for modern CPUs is guaranteed by cache coherence protocol (eg MESI) anyway, so what can volatile help here?可见性,对于现代 CPU 来说,缓存一致性协议(例如 MESI)无论如何都可以保证,那么volatile在这里有什么帮助呢?
  • Some articles say volatile variable uses memory directly instead of CPU cache, which guarantees visibility between threads.有些文章说 volatile 变量直接使用内存而不是 CPU 缓存,这保证了线程之间的可见性。 That doesn't sound a correct explain.这听起来不是一个正确的解释。
   Time : ---------------------------------------------------------->

 writer : --------- | write | -----------------------
reader1 : ------------- | read | -------------------- can I see the change?
reader2 : --------------------| read | -------------- can I see the change?

Hope I explained my question clearly.希望我清楚地解释了我的问题。

Relevant bits of the language spec:语言规范的相关部分:

volatile keyword: https://docs.oracle.com/javase/specs/jls/se16/html/jls-8.html#jls-8.3.1.4 volatile 关键字: https : //docs.oracle.com/javase/specs/jls/se16/html/jls-8.html#jls-8.3.1.4

memory model: https://docs.oracle.com/javase/specs/jls/se16/html/jls-17.html#jls-17.4内存模型: https : //docs.oracle.com/javase/specs/jls/se16/html/jls-17.html#jls-17.4

The CPU cache is not a factor here, as you correctly said.正如您所说,CPU 缓存不是这里的一个因素。

This is more about optimizations.这更多是关于优化。 If ready is not volatile, the compiler is free to interpret如果ready不是 volatile,则编译器可以自由解释

// this
while (!ready) {}

// as this
if (!ready) while(true) {}

That's certainly an optimization, it has to evaluate the condition fewer times.这当然是一种优化,它必须更少地评估条件。 The value is not changed in the loop, it can be "reused".该值在循环中不会改变,可以“重用”。 In terms of single-thread semantics it is equivalent, but it won't do what you wanted.就单线程语义而言,它是等效的,但它不会做你想要的。

That's not to say this would always happen.这并不是说这会一直发生。 Compilers are free to do that, they don't have to.编译器可以自由地这样做,他们不必这样做。

Visibility, for modern CPUs is guaranteed by cache coherence protocol (eg MESI) anyway, so what can volatile help here?可见性,对于现代 CPU 来说,缓存一致性协议(例如 MESI)无论如何都可以保证,那么 volatile 在这里有什么帮助呢?

That doesn't help you.那对你没有帮助。 You aren't writing code for a modern CPU, you are writing code for a Java virtual machine that is allowed to have a virtual machine that has a virtual CPU whose virtual CPU caches are not coherent.您不是在为现代 CPU 编写代码,而是在为 Java 虚拟机编写代码,该虚拟机允许具有虚拟 CPU 的虚拟机的虚拟 CPU 缓存不一致。

Some articles say volatile variable uses memory directly instead of CPU cache, which guarantees visibility between threads.有些文章说 volatile 变量直接使用内存而不是 CPU 缓存,这保证了线程之间的可见性。 That doesn't sound a correct explain.这听起来不是一个正确的解释。

That is correct.那是正确的。 But understand, that's with respect to the virtual machine that you are coding for.但是请理解,这与您正在编码的虚拟机有关。 Its memory may well be implemented in your physical CPU's caches.它的内存很可能在您的物理 CPU 的缓存中实现。 That may allow your machine to use the caches and still have the memory visibility required by the Java specification.这可能允许您的机器使用缓存并仍然具有 Java 规范所需的内存可见性。

Using volatile may ensure that writes go directly to the virtual machine's memory instead of the virtual machine's virtual CPU cache.使用volatile可以确保写入直接进入虚拟机的内存,而不是虚拟机的虚拟 CPU 缓存。 The virtual machine's CPU cache does not need to provide visibility between threads because the Java specification doesn't require it to.虚拟机的 CPU 缓存不需要提供线程之间的可见性,因为 Java 规范不需要它。

You cannot assume that characteristics of your particular physical hardware necessarily provide benefits that Java code can use directly.您不能假设您的特定物理硬件的特性一定会提供 Java 代码可以直接使用的好处。 Instead, the JVM trades off those benefits to improve performance.相反,JVM 会牺牲这些好处来提高性能。 But that means your Java code doesn't get those benefits.但这意味着您的 Java 代码无法获得这些好处。

Again, you are not writing code for your physical CPU , you are writing code for the virtual CPU that your JVM provides.同样,您不是为物理 CPU编写代码,而是为 JVM 提供的虚拟 CPU 编写代码。 That your CPU has coherent caches allows the JVM to do all kinds of optimizations that boost your code's performance, but the JVM is not required to pass those coherent caches through to your code and real JVM's do not.那你的CPU有连贯的高速缓存允许JVM做各种能够提升你的代码的性能优化,但要求JVM通过对你的代码和真正的JVM的通过那些连贯的缓存没有。 Doing so would mean eliminating a significant number of extremely valuable optimizations.这样做将意味着消除大量极其有价值的优化。

If ready is not defined volatile, is it possible that subscriber get stuck infinitely in the while loop?如果 ready 没有定义为 volatile,那么订阅者是否有可能无限地卡在 while 循环中?

Yes.是的。

Why?为什么?

Because the subscriber may not ever see the results of the publisher's write.因为订阅者可能永远不会看到发布者写入的结果。

Because ... the JLS does not require the value of an variable to be written to memory ... except to satisfy the specified visibility constraints.因为...... JLS不需要将变量的值写入内存......除非满足指定的可见性约束。

What does 'immediately visible' mean? “立即可见”是什么意思? Write operation takes some time, so after how long can other threads see volatile's change?写操作需要一些时间,那么其他线程多久能看到 volatile 的变化呢? Can a read in another thread that happens very shortly after the write starts but before the write finishes see the change?在写入开始后不久但在写入完成之前发生的另一个线程中的读取是否可以看到更改?

(I think) that the JMM specifies or assumes that it is physically impossible to read and write the same conceptual memory cell at the same time. (我认为)JMM 指定或假设在物理上不可能同时读取和写入相同的概念内存单元。 So operations on a memory cell are time ordered.因此,对存储单元的操作是按时间顺序进行的。 Immediately visible means visible in the next possible opportunity to read following the write.立即可见意味着在写入后的下一个可能的读取机会中可见。

Visibility, for modern CPUs is guaranteed by cache coherence protocol (eg MESI) anyway, so what can volatile help here?可见性,对于现代 CPU 来说,缓存一致性协议(例如 MESI)无论如何都可以保证,那么 volatile 在这里有什么帮助呢?

  1. Compilers typically generate code that holds variables in registers, and only writes the values to memory when necessary .编译器通常生成将变量保存在寄存器中的代码,并且仅在必要时将值写入内存。 Declaring a variable as volatile means that the value must be written to memory.将变量声明为volatile意味着该值必须写入内存。 If you take this into consideration, you cannot rely on just the (hypothetical or actual) behavior of cache implementations to specify what volatile means.如果考虑到这一点,则不能依赖缓存实现的(假设或实际)行为来指定volatile含义。

  2. While current generation modern CPU / cache architectures behave that way, there is no guarantee that all future computers will behave that way.虽然当前这一代现代 CPU / 缓存架构的行为方式如此,但并不能保证所有未来的计算机都会如此行为。

Some articles say volatile variable uses memory directly instead of CPU cache, which guarantees visibility between threads.有些文章说 volatile 变量直接使用内存而不是 CPU 缓存,这保证了线程之间的可见性。

Some people say that is incorrect ... for CPUs that implement a cache coherency protocol.有人说这是不正确的……对于实现缓存一致性协议的 CPU。 However, that is beside the point, because as I described above, the current value of a variable may not yet have been written to the cache yet.然而,这不是重点,因为如上所述,变量的当前值可能尚未写入缓存。 Indeed, it may never be written to the cache.实际上,它可能永远不会写入缓存。

 Time : ----------------------------------------------------------> writer : --------- | write | ----------------------- reader1 : ------------- | read | -------------------- can I see the change? reader2 : --------------------| read | -------------- can I see the change?

So lets assume that your diagram shows physical time and represents threads running on different physical cores, reading and writing a cache-coherent memory cell via their respective caches.因此,让我们假设您的图表显示物理时间并表示在不同物理内核上运行的线程,通过它们各自的缓存读取和写入缓存一致的内存单元。

What would happen at the physical level would depend on how the cache-coherency is implemented.在物理层会发生什么取决于缓存一致性是如何实现的。

I would expect Reader 1 to see the previous state of the cell (if it was available from its cache) or the new state if it wasn't.希望Reader 1 可以看到单元格的先前状态(如果它可以从其缓存中获得)或新状态(如果不是)。 Reader 2 would see the new state.读者 2 将看到新状态。 But it also depends on how long it takes for the writer thread's cache invalidation to propagate to the others' caches.但这也取决于写入线程的缓存失效传播到其他缓存所需的时间。 And all sorts of other stuff that is hard to explain.以及其他各种难以解释的东西。

In short, we don't really know what would happen at the physical level.简而言之,我们真的不知道在物理层面会发生什么。

But on the other hand, the writer and readers in the above picture can't actually observe the physical time like that anyway.但另一方面,上图中的作者和读者无论如何都无法真正观察到物理时间。 And neither can the programmer.程序员也不能。

What the program / programmer sees is that the reads and writes DO NOT OVERLAP.程序/程序员看到的是读写不重叠。 When the necessary happens before relations are present, there will be guarantees about visibility of memory writes by one thread to subsequent 1 reads by another.当必要的发生在关系存在之前,将保证一个线程的内存写入对另一个线程的后续1读取的可见性。 This applies for volatile variables, and for various other things.这适用于 volatile 变量,以及其他各种事物。

How that guarantee is implemented, is not your problem.如何实施保证,不是你的问题。 And it really doesn't help if you do understand what it going on at the hardware level, because you don't actually know what code the JIT compiler is going to emit (today!) anyway.如果您确实了解它在硬件级别上发生的事情,那真的无济于事,因为您实际上并不知道 JIT 编译器将发出什么代码(今天!)。


1 - That is, subsequent according to the synchronization order ... which you could view as a logical time. 1 - 也就是说,根据同步顺序随后......您可以将其视为逻辑时间。 The JLS Memory model doesn't actually talk about time at all. JLS 内存模型实际上根本不谈论时间。

Answers to your 3 questions:回答你的3个问题:

  1. A change of a volatile write doesn't need to be 'immediately' visible to a volatile load.易失性写入的更改不需要对易失性负载“立即”可见。 A correctly synchronized Java program will behave as if it is sequential consistent and for sequential consistency the real time order of loads/stores isn't relevant.正确同步的 Java 程序将表现得好像它是顺序一致的,并且对于顺序一致性,加载/存储的实时顺序是不相关的。 So reads and writes can be skewed as long as the program order isn't violated (or as long as nobody can observe it).因此只要不违反程序顺序(或只要没有人可以观察到它),读取和写入就可以倾斜。 Linearizability = sequential consistency + respect real time order.线性化 = 顺序一致性 + 尊重实时顺序。 For more info see this answer .有关更多信息,请参阅此答案

  2. I still need to dig into the exact meaning of visible, but AFAIK it is mostly a compiler level concern because hardware will prevent buffering loads/stores indefinitely.我仍然需要深入研究可见的确切含义,但 AFAIK 主要是编译器级别的问题,因为硬件将无限期地阻止缓冲加载/存储。

  3. You are completely right about the articles being wrong.你是完全正确的文章是错误的。 A lot of nonsense is written and 'flushing volatile writes to main memory instead of using the cache' is the most common misunderstanding I'm seeing.写了很多废话,“将易失性写入刷新到主内存而不是使用缓存”是我看到的最常见的误解。 I think 50% of all my SO comments is about informing people that caches are always coherent.我认为我所有的 SO 评论中有 50% 是关于告诉人们缓存总是连贯的。 A great book on the topic is 'A primer on memory consistency and cache coherence 2e' which is available for free .关于该主题的一本好书是“内存一致性和缓存一致性入门 2e”,可免费获得

The informal semantics of the Java Memory model contains 3 parts: Java 内存模型的非正式语义包含 3 个部分:

  • atomicity原子性
  • visibility能见度
  • ordering订购

Atomicity is about making sure that a read/write/rmw happens atomically in the global memory order.原子性是关于确保读/写/rmw 在全局内存顺序中原子地发生。 So nobody can observe some in between state.所以没有人可以观察到一些介于两者之间的状态。 This deals with access atomicity like torn read/write, word tearing and proper alignment.这涉及访问原子性,如读/写撕裂、字撕裂和正确对齐。 It also deals with operational atomicity like rmw.它还处理像 rmw 这样的操作原子性。

IMHO it should also deal with store atomicity;恕我直言,它还应该处理存储原子性; so making sure that there is a point in time where the store becomes visibly to all cores.因此,请确保有一个时间点,所有核心都可以看到存储。 If you have for example the X86, then due to load buffering, a store can become visible to the issuing core earlier than to other cores and you have a violation of atomicity.例如,如果您有 X86,那么由于负载缓冲,存储可以比其他内核更早地对发布内核可见,并且您违反了原子性。 But I haven't seen it being mentioned in the JMM.但我还没有看到 JMM 中提到它。

Visibility: this deals mostly with preventing compiler optimizations since the hardware will prevent delaying loads and buffering stores indefinitely.可见性:这主要是为了防止编译器优化,因为硬件将无限期地防止延迟加载和缓冲存储。 In some literature they also throw ordering of surrounding loads/stores under visibility;在一些文献中,他们还会在可见性下对周围的负载/商店进行排序; but I don't believe this is correct.但我不相信这是正确的。

Ordering: this is the bread and butter of memory models.排序:这是内存模型的基础。 It will make sure that loads/stores issued by a single processor don't get reordered.它将确保单个处理器发出的加载/存储不会被重新排序。 In the first example you can see the need for such behavior.在第一个示例中,您可以看到这种行为的必要性。 This is the realm of the compiler barriers and cpu memory barriers.这是编译器屏障和cpu内存屏障的领域。

For more info see: https://download.oracle.com/otndocs/jcp/memory_model-1.0-pfd-spec-oth-JSpec/有关更多信息,请参阅: https : //download.oracle.com/otndocs/jcp/memory_model-1.0-pfd-spec-oth-JSpec/

I'll just touch on this part :我只谈这部分:

change to ready on publisher thread is immediately visible to other threads在发布者线程上更改为就绪对其他线程立即可见

that is not correct and the articles are wrong.这是不正确的,文章是错误的。 The documentation makes a very clear statement here :文档在这里做了一个非常明确的声明:

A write to a volatile field happens-before every subsequent read of that field.对 volatile 字段的写入发生在对该字段的每次后续读取之前。

The complicated part here is subsequent .这里比较复杂的部分是后续 In plain english this means that when someone sees ready as being true , it will also see value as being 5 .通俗地说,这意味着当有人将ready视为true ,它也会将value视为5 This automatically implies that you need to observe that value to be true , and it can happen that you might observe a different thing.这自然意味着您需要观察该值是否为true ,并且您可能会观察到不同的事情。 So this is not "immediately".所以这不是“立即”。

What people confuse this with, is the fact that volatile offers sequential consistency , which means that if someone has observed ready == true , then everyone will also (unlike release/acquire , for example).人们对此感到困惑的是, volatile提供了顺序一致性,这意味着如果有人观察到ready == true ,那么每个人也会(例如,与release/acquire不同)。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM