简体   繁体   中英

Java -> volatile and final: Volatile as flushing-all-memory-content

Take a look on that answer here (1):

https://stackoverflow.com/a/2964277/2182302 ( Java Concurrency: Volatile vs final in "cascaded" variables? )

and on my old question here (2):

one java memoryFlushing volatile: A good programdesign?

So as i understand (see (2)) i can use volatile variables as memory barrier/flusher for ALL memory content not only for the referenced one by the volatile keyword.

now the accepted answer in (1) says that it would only flush the memory where the volatile-keyowrd is attached on.

So what is correct now?, and if the flushing-all principle in (2) is correct, why i cant then attach volatile to variables in combination with final?

Neither answer is correct, because you're thinking about it the wrong way. The concept of 'flush the memory' is simply made up. It's nowhere in the Java Virtual Machine Specification. It's just Not A Thing. Yes, many CPU/architectures do work that way, but the JVM does not.

You need to program to the JVM spec. Failure to do so means you write code that works perfectly fine on your machine, every time, and then you upload it to your server and it fails there. This is a horrible scenario: Buggy code, but bugs that cannot ever be trigged by tests. Yowza, those are bad.

So, what is in the JVM spec?

Not the concept of 'flushing'. What it does have, is the concept of HBHA: Happens-Before/Happens-After. Here's how it works:

  1. There is a list of specific interactions which sets up that some line of code is defined to 'happen before' (HB/HA = Happens before/Happens after) some other line. An idea of this list is given below.
  2. For any two lines which have an HBHA relationship, it would be impossible for the HA line to observe any state being such that it appears as if the HB line has not run yet. It's basically saying: HB lines occur before HA lines, except not quite that strong: You cannot observe the opposite (ie HB changes variable X, the HA line does not see this change to X, that'd be observing the opposite, that's impossible). Except timing-wise. In reality, HB/HA does not actually mean that lines get executed earlier or later: If you have 2 lines with an HB/HA relationship which have no effect on each other (one writes variable X. The other reads completely different variable Y), the JVM/CPU working together is free to reorder as much as it wants.
  3. For any two lines with no defined HB/HA relationship, the JVM and CPU are free to do whatever it pleases . Including things that just cannot be explained with a simplistic 'flushing' model.

For example:

int a = 0, b = 0;

void thread1() {
    a = 10;
    b = 20;
}

void thread2() {
    System.out.println(b);
    System.out.println(a);
}

In the above, no HB/HA relationship has been established between thread 1 modifying the state of a/b, and thread 2 reading them.

Therefore, it is legal for a JVM to print 20 0 , even though this cannot be explained with basic flushing notions: It is legal for the JVM to 'flush' b but not a.

It is somewhat unlikely for you to be capable of writing this code and actually observing that 20/0 print on any JVM version or any hardware, but the point is: It is allowed, and some day (or probably, it already exists), some exotic combo of JVM+hardware+OS version+state of the machine combines to actually make this happen, so if your code breaks if this sequence of events occurs, then you wrote a bug .

In effect, if one line mutates state, and another line reads it, and those 2 lines have no HB/HA, you messed up , and you need to fix your bug. Even (especially.) if you can't manage to write a test that actually proves it.

The trick here is that volatile reads do establish HB/HA, and as that is the only mechanism that the JVMS spec has to sync stuff up, yes, this has the effect of guaranteeing that you 'see all changes'. But this is not, at all, a good idea. Especially because the JVMS also says that the hotspot compiler is free to eliminate lines that have no side-effect.

So now we're going to have to get into a debate on whether 'establishes HBHA' is a side-effect. It probably is, but now we get to the rule of optimizations:

Write idiomatic code .

Whenever azul, the openjdk core dev team, etc are looking at improving the considerable optimization chops of the hotspot compiler, they look at real life code . It's like a gigantic pattern matcher: They look for patterns in code and finds ways to optimize them. They don't just write detectors for everything imaginable: They strongly prefer writing optimizers for patterns that commonly show up in real life java code. After all, what possible point is there spending time and effort optimizing a construction that almost no java code actually contains?

This gets us to the fundamental issue with using throw-away volatile reads as a way to establish HB/HA: Nobody does it that way , so the odds that at some point the JVMS is updated (or simply the conflicting rules are 'interpreted' as meaning: Yeah, hotspot can eliminate a pointless read, even if it did establish an HB/HA that is now no longer there) are quite high - you're also far more likely to run into JVM bugs if you do things in unique ways. After all, if you do things in ways that are well trodden, the bug would have been reported and fixed ages ago.

How to establish HB/HA:

  1. The natural rule: Within a single thread, code cannot be observed to run in any way except sequentially, ie within one thread, all lines have HB/HA with each other in the obvious fashion.

  2. synchronized blocks: If one thread exits a sync block and then another thread enters one on the same reference , then the sync-block-exit in A Happens-Before the sync-block-enter in B.

  3. volatile reads and writes.

  4. Some exotic stuff, such as: thread.start() happens-before the first line that thread's run() method, or all code in a thread is guaranteed to HB before thread.yield() on that thread finishes. These tend to be obvious.

Thus, to answer the question, is it good programming design?

No , it is not.

Establish HB/HA in the proper ways: Find something appropriate in java.util.concurrent and use it. From a simple lock to a queue to a fork/join pool for the entire job. Alternatively, stop sharing state. Alternatively, share state with mechanisms that are designed for concurrent access in more natural ways than HB/HA is, such as a database (transactions), or a message queue.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

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