Detailed semantics of volatile regarding timeliness of visibility
Asked Answered
S

5

24

Consider a volatile int sharedVar. We know that the JLS gives us the following guarantees:

  1. every action of a writing thread w preceding its write of value i to sharedVar in program order happens-before the write action;
  2. the write of value i by w happens-before the successful read of i from sharedVar by a reading thread r;
  3. the successful read of i from sharedVar by the reading thread r happens-before all subsequent actions of r in program order.

However, there is still no wall-clock time guarantee given as to when the reading thread will observe the value i. An implementation that simply never lets the reading thread see that value still complies with this contract.

I have thought about this for a while and I can't see any loopholes, but I assume there must be. Please, point out the loophole in my reasoning.

Swahili answered 1/8, 2012 at 14:34 Comment(16)
I always find things here that I would never think about. It's pretty cool.En
I don't understand, do you mean that the "reading thread" r is supposed to stop before reading i and never continue? I think that the JLS doesn't say anything about the fact the the program needs to make progress at any instruction..Allium
@Allium No, not stop. Simply read a stale value. Note that "stale" is not exactly a defined term since it is realtime-sensitive and the JLS avoids any realtime guarantees.Swahili
See, 17.4.4. Synchronization Order: A write to a volatile variable v (§8.3.1.4) synchronizes-with all subsequent reads of v by any thread (where "subsequent" is defined according to the synchronization order).Allium
Responding to your comment: > No, not stop. Simply read a stale value. Note that "stale" is not > exactly a defined term since it is realtime-sensitive and the JLS > avoids any realtime guarantees. A value is "stale" if the reading thread can detect the reordering. As long as the reordering cannot be detected, the JVM is free to perform the actions in whatever sequence it wants. As this page states: > the presence of a happens-before relationship between two actions does > not necessarily imply that they have to take placQuincyquindecagon
Perhaps the following discussion on the JMM mailing list may be of interest: cs.umd.edu/~pugh/java/memoryModel/archive/0619.htmlWestley
@Westley I think that discussion is much more specific than what I have in mind. If you'd like, read the answer that I have myself added to this page and see what you think of it.Swahili
This is all true, there are no real-time guarantees and there are no guarantees that a certain thread would ever win a contented CAS, and there is no guarantee a thread will get any scheduling - here is where the hardware and OS come. I can possibly elaborate more but at some point you need to consider the true iron.Siva
@Siva The main point here is: people routinely take for granted that the JLS guarantees instant visibility of a volatile write (and equivalent actions). This proves it doesn't guarantee anything of the sort. It is very important to know what is guaranteed and what is only reasonably expected from a known set of implementations.Swahili
well, JLS is an abstraction layer that has to run over some unknown hardware. Indeed abstractions are power illusions but the hardware is still there. There is no way to enforce the hardware to offer more 'visibility' just to meet people's expectation. However people can change the hardware and no one (sane at least) is going to develop a JVM on hardware that offers no benefits... or inherently/intentionally hamper the JVM to skip that 'visibility. Presently all JVMs/hardware combos I know of ensure that visibility at some point, usually as fast as the hardware allows for.Siva
@Siva I don't know how much you were following the discussion that led to the rewrite of the Java Memory Model with JLS Edition 3, but the very point of that rewrite was to formalize the guarantees that didn't exist but everyone relied on and, even more importantly, make clear what guarantees didn't exist, but were mistakenly assumed by many (the most famous example being of course the Double-Checked Locking idiom).Swahili
@MarkoTopolnik This has been bothering me for a while and I can't prove your answer wrong, even if it feels wrong. Have you considered presenting your results at [concurrency-interest] and see if anyone there sees anything that might have been missed here?Cassey
@Cassey Actually I have considered posting it somewhere, but haven't been sure exactly where's the right place. I'll google for concurrency-interest.Swahili
@Cassey Thanks for helping out with the direct links... I've joined the list and written up the question, but it just isn't getting published. There's no mention of moderation on the homepage, but there seems to be some in effect. I wonder what rituals one has to undergo to actually get his email published there :)Swahili
@MarkoTopolnik You are on and creating some headaches! Congrats!Cassey
what a fabulous question. I thought I am going mad myself with the same query. thank you so much for putting this one out.Verify
S
11

Turns out that the answers and the ensuing discussions only consolidated my original reasoning. I now have something in the way of a proof:

  1. take the case where the reading thread executes in full before the writing thread starts executing;
  2. note the synchronization order that this particular run created;
  3. now shift the threads in wall-clock time so they execute in parallel, but maintain the same synchronization order.

Since the Java Memory Model makes no reference to wall-clock time, there will be no obstructions to this. You now have two threads executing in parallel with the reading thread observing no actions done by the writing thread. QED.

Example 1: One writing, one reading thread

To make this finding maximally poignant and real, consider the following program:

static volatile int sharedVar;

public static void main(String[] args) throws Exception {
  final long startTime = System.currentTimeMillis();
  final long[] aTimes = new long[5], bTimes = new long[5];
  final Thread
    a = new Thread() { public void run() {
      for (int i = 0; i < 5; i++) {
        sharedVar = 1;
        aTimes[i] = System.currentTimeMillis()-startTime;
        briefPause();
      }
    }},
    b = new Thread() { public void run() {
      for (int i = 0; i < 5; i++) {
        bTimes[i] = sharedVar == 0?
            System.currentTimeMillis()-startTime : -1;
        briefPause();
      }
    }};
  a.start(); b.start();
  a.join(); b.join();
  System.out.println("Thread A wrote 1 at: " + Arrays.toString(aTimes));
  System.out.println("Thread B read 0 at: " + Arrays.toString(bTimes));
}
static void briefPause() {
  try { Thread.sleep(3); }
  catch (InterruptedException e) {throw new RuntimeException(e);}
}

As far as JLS is concerned, this is a legal output:

Thread A wrote 1 at: [0, 2, 5, 7, 9]
Thread B read 0 at: [0, 2, 5, 7, 9]

Note that I don't rely on any malfunctioning reports by currentTimeMillis. The times reported are real. The implementation did choose, however, to make all actions of the writing thread visible only after all the actions of the reading thread.

Example 2: Two threads both reading and writing

Now @StephenC argues, and many would agree with him, that happens-before, even though not explicitly mentioning it, still implies a time ordering. Therefore I present my second program that demonstrates the exact extent to which this may be so.

public static void main(String[] args) throws Exception {
  final long startTime = System.currentTimeMillis();
  final long[] aTimes = new long[5], bTimes = new long[5];
  final int[] aVals = new int[5], bVals = new int[5];
  final Thread
    a = new Thread() { public void run() {
      for (int i = 0; i < 5; i++) {
        aVals[i] = sharedVar++;
        aTimes[i] = System.currentTimeMillis()-startTime;
        briefPause();
      }
    }},
    b = new Thread() { public void run() {
      for (int i = 0; i < 5; i++) {
        bVals[i] = sharedVar++;
        bTimes[i] = System.currentTimeMillis()-startTime;
        briefPause();
      }
    }};
  a.start(); b.start();
  a.join(); b.join();
  System.out.format("Thread A read %s at %s\n",
      Arrays.toString(aVals), Arrays.toString(aTimes));
  System.out.format("Thread B read %s at %s\n",
      Arrays.toString(bVals), Arrays.toString(bTimes));
}

Just to help understanding the code, this would be a typical, real-world result:

Thread A read [0, 2, 3, 6, 8] at [1, 4, 8, 11, 14]
Thread B read [1, 2, 4, 5, 7] at [1, 4, 8, 11, 14]

On the other hand, you'd never expect to see anything like this, but it is still legit by the standards of the JMM:

Thread A read [0, 1, 2, 3, 4] at [1, 4, 8, 11, 14]
Thread B read [5, 6, 7, 8, 9] at [1, 4, 8, 11, 14]

The JVM would actually have to predict what Thread A will write at time 14 in order to know what to let the Thread B read at time 1. The plausibility and even feasibility of this is quite dubious.

From this we can define the following, realistic liberty that a JVM implementation can take:

The visibility of any uninterrupted sequence of release actions by a thread can be safely postponed until before the acquire action that interrupts it.

The terms release and acquire are defined in JLS §17.4.4.

A corrollary to this rule is that the actions of a thread which only writes and never reads anything can be postponed indefinitely without violating the happens-before relationship.

Clearing up the volatile concept

The volatile modifier is actually about two distinct concepts:

  1. The hard guarantee that actions on it will respect the happens-before ordering;
  2. The soft promise of a runtime's best effort towards a timely publishing of writes.

Note the point 2. is not specified by the JLS in any way, it just kind of arises by general expectation. An implementation that breaks the promise is still compliant, obviously. With time, as we move to massively parallel architectures, that promise may indeed prove to be quite flexible. Therefore I expect that in the future the conflation of the guarantee with the promise will prove to be insufficient: depending on requirement, we'll need one without the other, one with a different flavor of the other, or any number of other combinations.

Swahili answered 1/8, 2012 at 18:52 Comment(18)
Are you saying that you actually get those results when you run the program? Because I get something very different: "Writing 1 at 1 Reading 1 at 1 Writing 1 at 4 Reading 1 at 4 Writing 1 at 7 Reading 1 at 8 Writing 1 at 10 Reading 1 at 11 Writing 1 at 13 Reading 1 at 14"Branen
The only way I think you could get those results is if there was not a "happens before" relationship between the hardware updating the clock and a call to System.currentTimeMillis(). And that surely must exist or we can't rely on System.currentTimeMillis() to deliver a reliable answer.Branen
In reality, you could have all the read events returning zero, but if that happened the read times would all be before the write times. Even if the JLS doesn't say anything to that effect, when you add the semantics of the system clock ... and the associated "happens before" ... it has to be that way.Branen
@StephenC System.currentTimeMillis() imposes no happens-before relationship, and there is no compelling reason for it to do so; in fact, it could even be detrimental to performance. I don't see any reason why that would prevent it from delivering a reliable result. "Hardware updating the clock" is not an action covered by the JMM and there is no contradiction of my results with the JLS or with the Javadoc specification of System.currentTimeMillis. BTW no, these are not the results I could actually reproduce on my JVM.Swahili
@StephenC "it has to be that way" -- it would have to be that way only if happens-before was indeed a temporal ordering, which it simply is not.Swahili
Well if you are so sure of this, report it to Oracle as a bug in the JLS. Because if the nonsensical behaviour you claim that it allows is actually allowed, then it is a bug. However, your apparent claim that there is no connection between time and causality does seem to be a weak point in your argument ... to say the least.Branen
@StephenC If the point is so weak then why do you and everyone else fail to disprove it? This is in fact the precise reason I posed this question: find the loophole in my reasoning. Conclusion: there is no loophole. Also, you still keep mixing the physical definitions of time and causality with concepts from the JMM that have nothing to do with those. JMM's action has no one-to-one mapping with a real-world event---hence no contradiction, no nonsense, and no bug.Swahili
I think I have disproved it, but you refuse to see that there must be a connection between the JMM and the physical world in which JMM implementations must exist. Anyway, it is not really relevant to anyone but the people who wrote the JLS. Even if you have found a theoretical problem with the JLS, it is not relevant to people who use real Java implementations. In the real world, JVMs don't behave like your example.Branen
@StephenC There was a time (before JLS 3) when people assumed the guarantee that a thread would never see the actions of the other threads out of order. When a volatile var was involved, they happened to be right, in other cases they were wrong. Assuming guarantees that are not there is always a dangerous practice. It just so happens that a volatile write will be immediately visible on an SMP architecture because that's the natural way to implement it. When some other arch comes into fashion where this is not so, the facts that I expose here may suddenly become the focus of much attention.Swahili
@StephenC You have not disproved anything; you have simply stated there must be a temporal ordering involved. You did not provide a single bit of evidence for your claim: the closest you got was offering an ill-conceived analogy of JMM actions with physical events, the rebuttal of which I have phrased and rephrased several times over. If I were to agree with you at this point, I could base that only on blind faith in Your words.Swahili
IMO this entire discussion is disconnected from reality. Open an Oracle bug report, or try to get an academic paper published on it. I'm not spending any more time on this topic.Branen
@StephenC No problem, and thank you for your time. You have helped me refine my concepts.Swahili
@StephenC I have expanded my answer to give full weight to your position. The new conclusion is quite uncontroversial and I think you will find no reason to contend it, yet it retains its original strength of a non-trivial and important observation.Swahili
Hi. Regarding the output Thread A read [0, 1, 2, 3, 4] at [1, 4, 8, 11, 14] Thread B read [5, 6, 7, 8, 9] at [1, 4, 8, 11, 14] of example 2, I guess there is some special thing about System.currentTimeMillis(), maybe an implicit 'happens-before' relationship, to make sure that System.currentTimeMillis() is ordered after the read of bVals in an execution. Then we could get output like Thread B read [5, 6, 7, 8, 9] at [17, 20, 23, 26, 29], which we would expect.Superordinate
However, I'm not sure what is special about System.currentTimeMillis(), so I opened a question to discuss. Please share your thoughts on this with us. :)Superordinate
As implied in this post on the concurrency-interest thread initiated by my question here, there is nothing (yet) special about currentTimeMillis().Swahili
Wow, lots of discussions on that thread; I'm gonna dig into it. Again, thanks for your sharing, :)Superordinate
I think this is a really interesting discussion which puts the spotlight on what the JMM is and isn't. I think the JMM describes how correct executions should behave. The JMM does not make any statements about liveliness or performance. Such things might be described in other places, such as in the technical specification of various JVM:s.Clothesline
C
4

You are partly correct. My understanding is that this would be legal though if and only if thread r did not engage in any other operations that had a happens-before relationship relative to thread w.

So there's no guarantee of when in terms of wall-clock time; but there is a guarantee in terms of other synchronisation points within the program.

(If this bothers you, consider that in a more fundamental sense, there is no guarantee that the JVM will ever actually execute any bytecode in a timely fashion. A JVM that simply stalled forever would almost certainly be legal, because it's essentially impossible to provide hard timing guarantees on execution.)

Comehither answered 1/8, 2012 at 14:50 Comment(7)
Let's say that there is complete isolation between threads, not a single write action observed by another thread. Would there be any contradiction with the happens-before ordering?Swahili
If there's complete isolation between threads (other than, presumably, the read of the volatile variable) then no contradiction with happens-before is even theoretically possible, because there is only one event that is eligible for such an ordering, and only one way it can be ordered!Comehither
No, I mean if there are writes and reads as described, but the actual execution of two threads happens in total isolation, but in parallel.Swahili
If there are at least two "things" in thread w that establish happens-before relationships w.r.t. thread r, then r must see them occur in that order. IF w updated sharedVar and then sharedVar2, r would never be able to read the new value for the second var but the stale value for the first. You are right that threads run in isolation though; one thread does not know where the other has got to other than by what it can observe. The JLS merely imposes a consistent ordering on cross-thread effects, with no wall-time guarantees as to when this visibility occurs.Comehither
This is the context in which this is most worrying: you know the usual warnings "unless you use volatile, Java gives you no guarantee that the reading thread will ever observe the written value". Well guess what, with or without volatile there are no guarantees!Swahili
I see what you mean, but that's not entirely true as it does establish an ordering guarantee, just not a timeliness guarantee. It means that from thread r's perspective, everything that you can observe about thread w will be consistent with it being at a particular point in its execution. As for your closing comment, I think it's still just a specific case of my closing paragraph above. If I write foo(); bar(); then we say that bar gets called after foo, but there are still no guarantees it ever gets called at all!Comehither
Yes, we basically agree on all points. I am just stressing this aspect of wall-time guarantees because that aspect is indeed very important when you want inter-thread communication. Now, you can say that this is never an issue in practice, but many discussions are lead exclusively at the JLS level, assuming that the JLS itself does give wall-time guarantees. This can always lead to danger, assuming something is guaranteed when it isn't.Swahili
I
3

Please see this section (17.4.4). you have twisted the specification a bit, which is what is confusing you. the read/write specification for volatile variables says nothing about specific values, specifically:

  • A write to a volatile variable (§8.3.1.4) v synchronizes-with all subsequent reads of v by any thread (where subsequent is defined according to the synchronization order).

UPDATE:

As @AndrzejDoyle mentions, you could conceivably have thread r read a stale value as long as nothing else that thread does after that point establishes a synchronization point with thread w at some later point in the execution (as then you would be in violation of the spec). So yes, there is some wiggle room there, but thread r would be very restricted in what it could do (for instance, writing to System.out would establish a later sync point as most stream impls are synchronized).

Inventor answered 1/8, 2012 at 14:44 Comment(8)
But what constrains the synchronization order so that the write to a volatile synchronizes-with any particular read?Swahili
@MarkoTopolnik - i see where you are going, updated my answerInventor
RE your update: but isn't that just circular reasoning? What would make that other action establish a sync point, if what we have discussed so far didn't necessarily establish it?Swahili
@MarkoTopolnik - it did establish a sync point, just one "earlier" in time. even reading a stale value establishes a sync point, you've just moved it back in the total execution order. in order to not violate the spec, you must be able to show a consistent total order for all sync points in the program execution.Inventor
Correct. But what would make that other action establish a sync point at such a place that would of necessity force the one established by the volatile read to observe the new value? That other point may as well lie in the past, don't you think?Swahili
@MarkoTopolnik - yes, you are correct. you can keep moving the sync points back in time, as long as you can keep the total order consistent. but, like i said, that becomes increasingly difficult the more things that happen (like printing to System.out).Inventor
That's what I'm thinking right now. You can imagine two threads running side-by-side in the wall-clock sense, but the happens-before being formally established from the end of thread r to the start of thread w. In that case the specification would be satisfied and there would be no observed writes.Swahili
@MarkoTopolnik - added an example.Inventor
I
3

I don't believe any of the below anymore. It all comes down to the meaning of "subsequent," which is undefined except for two mentions in 17.4.4, where it's tautologically "defined according to the synchronization order".)

The only thing we really have to go on is in section 17.4.3:

Sequential consistency is a very strong guarantee that is made about visibility and ordering in an execution of a program. Within a sequentially consistent execution, there is a total order over all individual actions (such as reads and writes) which is consistent with the order of the program, and each individual action is atomic and is immediately visible to every thread. (emphasis added)

I think there is such a real-time guarantee, but you have to piece it together from various sections of JLS chapter 17.

  1. According to section 17.4.5, "the happens-before relation defines when data races take place." It doesn't seem to be explicitly stated, but I assume that this means that if an action a happens-before another action a', there is no data race between them.
  2. According to 17.4.3: "A set of actions is sequentially consistent if ... each read r of a variable v sees the value written by the write w to v such that w comes before r in the execution order ... If a program has no data races, then all executions of the program will appear to be sequentially consistent."

If you write to a volatile variable v and subsequently read from it in another thread, that means that the writes happens-before the read. That means that there is no data race between the write and the read, which means they must be sequentially consistent. That means the read r must see the value written by the write w (or a subsequent write).

Interatomic answered 17/8, 2012 at 21:22 Comment(1)
Now just note that sequential consistency is NOT the guarantee of the JMM, in fact it is quite the opposite: the JMM specifies the rules by which execution can proceed in a sequentially inconsistent manner without hurting program correctness. Quote JLS just below yours: "If we were to use sequential consistency as our memory model, many of the compiler and processor optimizations that we have discussed would be illegal."Swahili
P
1

I think the volatile in Java is expressed in terms of "if you see A you will also see B".

To be more explicit, Java promises that when you thread reads a volatile variable foo and sees value A, you have some guarantees as to what you will see when you read other variables later on the same thread. If the same thread that wrote A to foo also wrote B to bar (before writing A to foo), you're guaranteed to see at least B in bar.

Of course, if you never get to see A, you can't be guaranteed to see B either. And if you see B in bar, that says nothing about the visibility of A in foo. Also, the time that elapses between the thread writing A to foo and another thread seeing A in foo is not guaranteed.

Premolar answered 19/12, 2014 at 21:49 Comment(1)
I think this is a very nice and simple explanation of the very thing that Marko Topolnik talks about in his question and answer. But he makes it sound much more complicated than necessary.Clothesline

© 2022 - 2024 — McMap. All rights reserved.