Reputation: 11
Depending on the source I've heard different things. Some say that volatile updates to shared cache, but most others have said it updates to ram(main memory) and is read from ram. Clearly if you are writing and reading from ram that will trash performance badly. Given modern processors have shared multicore caches, does writing variables only update local caches, and doesn't update the shared cache? Does volatile correct this issue at the cache level or as most say does it rely on main memory?
If variables are not updated at the shared cache level, is there a technical reason why this is so? It would seem that updating and reading from shared cache would have higher performance than updating and reading from main memory.
I'm trying to understand better multithread code, and performance implications. Haven't run into any issue.
Upvotes: 1
Views: 115
Reputation: 72258
Modern CPUs have fully coherent cache lines.
volatile
is primarily a language feature for compiling ASM. Pretty much all that it does is force the write to go to a memory address as opposed to registers, it doesn't decide whether or where that memory address is cached. The actual write may be to a global or local cache line, or to main memory.
It may also introduce a write barrier to prevent re-ordering of instructions. It also forces the write to be atomic.
It does not do anything involving the cache lines. The CPU core manages its local cache lines, and ensures that they are consistent with other local cache lines, the global cache line, and main memory at all times. Consistent here does not mean that they are all the same. It means that at any point, a core will know whether a particular location is valid to be read or written to, and therefore where the source of truth is for a particular memory address is.
Upvotes: 0
Reputation: 103668
This just isn't how java-the-language works. java-the-language has an official spec and that spec isn't written with a specific processor in mind.
The controlling document is the Java Memory Model.
The JMM works in terms of guarantees. In general, the JMM works as follows:
And these things are intentionally kept very abstract. A JVM implementation therefore just needs to ensure that first bullet is guaranteed and can do whatever it wants otherwise. Hence, something like volatile
simply guarantees that it is no longer possible to observe the update from one thread and not from another, and how a JVM ensures this guarantee is up to the JVM.
This is why java can be efficient.
Here's a simple example:
The JMM guarantees the following:
There is an enumerated list of 'Happens Before' causes, each entry is highly specced. Any 2 lines (technically, any 2 bytecode instructions) either have an HB relationship or not, and you can know for sure, because it's specced. Let's say we have Bytecode1 and Bytecode2, and bc1 has a Happens-Before relationship with bc2.
A JVM guarantees that it is impossible to observe state as it was before BC1 finished execution when BC2 executes, in that context.
And that is all that a JVM guarantees.
For example, someThread.start()
is specced to be HB relative to the first line in the run()
method of that thread. Hence, given:
class Test {
static long v = 10;
void test() {
v = -1;
new Thread(() -> {
System.out.println(v);
}).start();
}
}
The above code Must print -1. If it prints 20, that JVM is buggy; if you file a bug about it, it would be accepted. That's because v = -1;
is happens-before relative to .start()
, and thus, being able to observe state as it was before v = -1;
is a thing the JVM spec guarantees cannot happen.
In contrast:
class Test {
static long v = 10;
void test() {
new Thread(() -> {
Thread.sleep(1000);
System.out.println(v);
}).start();
v = -1;
}
}
Here the JVM spec says that a JVM that prints '10' is legal. A JVM that prints '-1' is also legal. A JVM that flips a coin to determine this, is legal. A JVM that decides to print -1 every day and in every unit test today even if you run it 10000 times, and then juuuust as you give a demo to that important customer it starts returning 10... that'd be legal.
In fact, the JVM spec even says that getting a split read is legal (where the first 32 bits are all 1-bits due to -1
being all 1 bits, and the latter 32 bits are still 20, resulting in some large negative number). This is an interesting case: Pretty much no JVM you could possibly run today can be coerced to actually produce that split read, because who still has full 32-bit outlays, and even then, often you simply can't catch the other thread 'in between'.
Nevertheless, if you write code that would break if a split read condition occurs, that code is buggy. If a tree falls in the forest... - if you program a bug that cannot possibly be triggered anymore, is it a bug? I dunno, ask a philosopher :)
File bugs all day about this and it'll just be denied: The JVM is working fine, because it adheres to the spec.
Even if you expand the sleep to '14 days' and just let the JVM run that long, it could print '10', even though that seems nuts (it got set to -1 14 days ago!), but there are JVMs out there that really will do that.
volatile
is a bit more convoluted in exactly which guarantees you get, and it also fits in this Happens-Before framework (essentially, any 2 bytecode instructions that interact with a volatile field, at least one of which is a write instruction, establish HB but can be a little tricky to know which one is the Happens-Before).
HOW does the JVM implement this? That's up to the JVM. If there is an efficient way to provide that guarantee, it'll do the efficient thing.
This is exactly why the JVM is specced so abstractly. Imagine the spec literally spelled out: volatile
ensures that any write to the field will be immediately flushed out to main RAM.
Then if there was a much faster way to ensure inter-thread consistency by e.g. only flushing pages from one core directly to another (imagine a processor exists that could do such a thing), or there's a cache level shared by all cores and you can flush only to there - then a JVM could not use those functionalities because some well-meaning moron decided to get overly specific about some explicit CPU in the Java Language Specification document.
For a breakdown of how most JVMs currently implement the requirement to guarantee certain behaviours when volatile
is used, see @Charlieface's answer. But realize that that answer is not actually what the java lang spec guarantees in any way; the JLS doesn't even mention the word 'cache line'.
Upvotes: 1
Reputation: 27190
The Java Language Specification (JLS) says almost nothing about "main memory" or "cache." It describes the behavior of volatile
variables strictly in terms of how it restricts the ordering of events in different threads.
The JLS description is kind of dry and academic. (search for "volatile" wherever it appears in chapter 17.) But mostly, what it tells you is; Whatever thread A does before it writes to some volatile
field f
must become visible to another thread B by the time thread B reads the same field f
and gets the value that A wrote.
Upvotes: 0