7

In the oracle Java documentation located here, the following is said:

Atomic actions cannot be interleaved, so they can be used without fear of thread interference. However, this does not eliminate all need to synchronize atomic actions, because memory consistency errors are still possible. Using volatile variables reduces the risk of memory consistency errors, because any write to a volatile variable establishes a happens-before relationship with subsequent reads of that same variable. This means that changes to a volatile variable are always visible to other threads. What's more, it also means that when a thread reads a volatile variable, it sees not just the latest change to the volatile, but also the side effects of the code that led up the change.

It also says:

  • Reads and writes are atomic for reference variables and for most primitive variables (all types except long and double).
  • Reads and writes are atomic for all variables declared volatile (including long and double variables).

I have two questions regarding these statements:

  1. "Using volatile variables reduces the risk of memory consistency errors" - What do they mean by "reduces the risk", and how is a memory consistency error still possible when using volatile?

  2. Would it be true to say that the only effect of placing volatile on a non-double, non-long primitive is to enable the "happens-before" relationship with subsequent reads from other threads? I ask this since it seems that those variables already have atomic reads.

John Humphreys
  • 37,047
  • 37
  • 155
  • 255
  • 6
    'volatile' - the keyword that keeps on giving! – Mitch Wheat Jun 30 '14 at 14:11
  • 3
    It seems to me you probably don't understand something very well yourself if you can't directly address a very specific quote in Java documentation. In any case a snarky comment with no attempted advice isn't great use of stack overflow's servers, now is it? If the answer is "it's too complex to explain but here are good references." then that is completely fine. – John Humphreys Jun 30 '14 at 14:18
  • 3
    @Holger I suspect there is a short enough answer out there that can address Q1. Q2 is also likely to be extremely short (i.e. "Yes, that's correct".) – Duncan Jones Jun 30 '14 at 14:18
  • 1
    @w00te Q1 likely answered by [Example of a memory consistency error when using volatile keyword?](http://stackoverflow.com/a/13598456/474189) – Duncan Jones Jun 30 '14 at 14:20
  • @HotLicks - I bet atleast *Jon Skeet* or *HoverCroft Full of Eels* do :P. – TheLostMind Jun 30 '14 at 14:21
  • Thanks Duncan; that does look like a good answer to Q1 :). – John Humphreys Jun 30 '14 at 14:21
  • @Duncan: Q2 might be answered shortly, however, you can’t answer Q1 in a compact matter as it would not be enough to name *one example* given the question as it is written. And to a questioner who seems to think, atomicity was the only one issue with concurrency, the short answer would be rather misleading. – Holger Jun 30 '14 at 14:25
  • @TheLostMind actually both of those people will be the first to admit that their understanding of the jmm is incomplete I'd say ;) anyhow this is the prime example for a way too broad question I think – Voo Jun 30 '14 at 14:30
  • 1
    @w00te: Ok, so if you wish to hear it: “it's too complex to explain but there are good references.” If you tried to research by yourself, you already knew that. However, the often cited “Java Concurrency in Practice” by Brian Goetz, Joshua Bloch, Doug Lea, et al. is one of the best resources… But normally, asking for such literature is considered off-topic on SO. – Holger Jun 30 '14 at 14:36
  • 1
    I think the JLS wording is misleading. The term "memory consistency error" usually refers only to visibility issues, not the atomicity of complex operations. The `volatile` keyword completely eliminates the former, but does nothing at all to address the latter. – Kevin Krumwiede Jun 30 '14 at 17:04

6 Answers6

4

What do they mean by "reduces the risk"?

Atomicity is one issue addressed by the Java Memory Model. However, more important than Atomicity are the following issues:

  • memory architecture, e.g. impact of CPU caches on read and write operations
  • CPU optimizations, e.g. reordering of loads and stores
  • compiler optimizations, e.g. added and removed loads and stores

The following listing contains a frequently used example. The operations on x and y are atomic. Still, the program can print both lines.

int x = 0, y = 0;

// thread 1
x = 1
if (y == 0) System.out.println("foo");

// thread 2
y = 1
if (x == 0) System.out.println("bar");

However, if you declare x and y as volatile, only one of the two lines can be printed.


How is a memory consistency error still possible when using volatile?

The following example uses volatile. However, updates might still get lost.

volatile int x = 0;

// thread 1
x += 1;

// thread 2
x += 1;

Would it be true to say that the only effect of placing volatile on a non-double, non-long primitive is to enable the "happens-before" relationship with subsequent reads from other threads?

Happens-before is often misunderstood. The consistency model defined by happens-before is weak and difficult to use correctly. This can be demonstrated with the following example, that is known as Independent Reads of Independent Writes (IRIW):

volatile int x = 0, y = 0;

// thread 1
x = 1;

// thread 2
y = 1;

// thread 3
if (x == 1) System.out.println(y);

// thread 4
if (y == 1) System.out.println(x);

Only with happens-before, two 0s would be valid result. However, that's apparently counter-intuitive. For that reason, Java provides a stricter consistency model, that forbids this relativity issue, and that is known as sequential consistency. You can find it in sections §17.4.3 and §17.4.5 of the Java Language Specification. The most important part is:

A program is correctly synchronized if and only if all sequentially consistent executions are free of data races. If a program is correctly synchronized, then all executions of the program will appear to be sequentially consistent (§17.4.3).

That means, volatile gives you more than happens-before. It gives you sequential consistency if used for all conflicting accesses (§17.4.3).

MyStackRunnethOver
  • 4,872
  • 2
  • 28
  • 42
nosid
  • 48,932
  • 13
  • 112
  • 139
  • you mean to say that if I use *volatile* , the values of x and y read by thread-3 and thread-4 can never be 0? – TheLostMind Jun 30 '14 at 18:14
  • @TheLostMind: They can not be `0` at the same time. With _sequential consistency_, the valid results are `0,1`, `1,0` and `1,1`. With **only** _happens-before_, `0,0` would also be a valid result. – nosid Jun 30 '14 at 18:25
  • 1
    down vote: Why does the first code snippet print the only one of the two lines? I don't understand why. I imagine the following scenario: 1) thread 1 writes `x` to 1. 2) thread 2 writes `y` to 1. 3) thread 1 executes the if statement(false). 4) thread 2 execute the if statement(false). So, get no result. What's wrong with my comment? – inherithandle May 19 '15 at 15:36
  • @inherithandle: With _only one line_ I mean _not more than one line_. Of course you are right, the program can print nothing at all. However, the really interesting case is whether the program can print _both_ lines in one run. – nosid May 19 '15 at 20:55
1

The usual example:

while(!condition)
    sleep(10);

if condition is volatile, this behaves as expected. If it is not, the compiler is allowed to optimize this to

if(!condition)
    for(;;)
        sleep(10);

This is completely orthogonal to atomicity: if condition is of a hypothetical integer type that is not atomic, then the sequence

thread 1 writes upper half to 0
thread 2 reads upper half (0)
thread 2 reads lower half (0)
thread 1 writes lower half (1)

can happen while the variable is updated from a nonzero value that just happens to have a lower half of zero to a nonzero value that has an upper half of zero; in this case, thread 2 reads the variable as zero. The volatile keyword in this case makes sure that thread 2 really reads the variable instead of using its local copy, but it does not affect timing.

Third, atomicity does not protect against

thread 1 reads value (0)
thread 2 reads value (0)
thread 1 writes incremented value (1)
thread 2 writes incremented value (1)

One of the best ways to use atomic volatile variables are the read and write counters of a ring buffer:

thread 1 looks at read pointer, calculates free space
thread 1 fills free space with data
thread 1 updates write pointer (which is `volatile`, so the side effects of filling the free space are also committed before)
thread 2 looks at write pointer, calculates amount of data received
...

Here, no lock is needed to synchronize the threads, atomicity guarantees that the read and write pointers will always be accessed consistently and volatile enforces the necessary ordering.

Simon Richter
  • 28,572
  • 1
  • 42
  • 64
  • I didn't really understand - are you stating that word-tearing is possible with volatile variables? – Ordous Jun 30 '14 at 14:53
  • 1
    In principle, yes, because the concepts are unrelated. In practice, the Java VM has rather strong atomicity guarantees on its own, so you will find it difficult to apply `volatile` to a non-atomic type. – Simon Richter Jun 30 '14 at 15:08
  • "atomicity does not protect against ..." Did you mean to say, `volatile` does not protect against...? The example you give looks like the very definition of "atomic." When we say that some operation is "atomic", we mean that other threads can see the state before, or the state after, but they are guaranteed never to see an intermediate/inconsistent/wrong state. – Solomon Slow Jun 30 '14 at 15:18
  • No, my point is that even if the access is atomic, this does not mean that read-modify-write accesses will be as well. – Simon Richter Jun 30 '14 at 15:19
  • @SimonRichter JLS specifically states "Writes and reads of volatile long and double values are always atomic.". Word tearing cannot occur for any other primitive values or references as well. Hence volatility gives absolute defense against word tearing – Ordous Jun 30 '14 at 15:28
  • That's what I meant with "strong atomicity guarantees". I'm aware that I'm language-lawyering here, but I'm trying to give an answer that not only works for Java, but also extends to the C and C++ memory models consistently. – Simon Richter Jun 30 '14 at 16:46
  • @SimonRichter - Isn't word tearing contradictory to Atomicity?. I don't quite understand your examples... – TheLostMind Jun 30 '14 at 18:17
  • Precisely. My point is that it's not the `volatile` keyword that protects you from word tearing, but rather that the data type can be accessed atomically. – Simon Richter Jul 01 '14 at 10:59
1

For question 1, the risk is only reduced (and not eliminated) because volatile only applies to a single read/write operation and not more complex operations such as increment, decrement, etc.

For question 2, the effect of volatile is to make changes immediately visible to other threads. As the quoted passage states "this does not eliminate all need to synchronize atomic actions, because memory consistency errors are still possible." Simply because reads are atomic does not mean that they are thread safe. So establishing a happens before relationship is almost a (necessary) side-effect of guaranteeing memory consistency across threads.

Brett Okken
  • 6,210
  • 1
  • 19
  • 25
0

Ad 1: With a volatile variable, the variable is always checked against a master copy and all threads see a consistent state. But if you use that volatility variable in a non-atomic operation writing back the result (say a = f(a)) then you might still create a memory inconsistency. That's how I would understand the remark "reduces the risk". A volatile variable is consistent at the time of read, but you still might need to use a synchronize.

Ad 2: I don't know. But: If your definition of "happens before" includes the remark

This means that changes to a volatile variable are always visible to other threads. What's more, it also means that when a thread reads a volatile variable, it sees not just the latest change to the volatile, but also the side effects of the code that led up the change.

I would not dare to rely on any other property except that volatile ensures this. What else do you expect from it?!

Christian Fries
  • 16,175
  • 10
  • 56
  • 67
0

Assume that you have a CPU with a CPU cache or CPU registers. Independent from your CPU architecture in terms of number of cores it has, volatile does NOT guarantee you a perfect inconsistency. The only way to achieve this is to use synchronized or atomic references with a performance price.

For example you have multiple threads (Thread A & Thread B) working on a shared data. Assume that Thread A wants to update the shared data and it's is started .For performance reasons, Thread A's stack was moved to CPU cache or registers. Then Thread A updated the shared data. But the problem with those places is that actually they don't flush back the updated value to the main memory immediately. This is where inconsistency's offered because up to the flash back operation, Thread B might have wanted to play with the same data, which would have taken it from the main memory - yet unupdated value.

If you use volatile all the operations will be perfomed on the main memory so you don't have a flush back latency. But, this time you may suffer from thread pipeline. In the middle of write operation (composed of number of atomic operations), Thread B may have been executed by the os to perform a read operation and that's it! Thread B will read the unupdated value again. That's why it's said it reduces the risk.

Hope you got it.

stdout
  • 2,471
  • 2
  • 31
  • 40
-1

when coming to concurrency, you might want to ensure 2 things:

  • atomic operations: a set of operations is atomic - this is usually achieved with "synchronized" (higher level constructs). Also with volatile for instance for read/write on long and double.

  • visibility: a thread B sees a modification made by a thread A. Even if an operation is atomic, like a write to an int variable, a second thread can still see a non-up-to-date value of the variable, due to processor caches. Putting a variable as volatile ensures that the second thread does see the up-to-date value of that variable. More than that, it ensures that the second thread sees an up-to-date value of ALL the variables written by the first thread before the write to the volatile variable.

Emil Salageanu
  • 997
  • 1
  • 10
  • 24