The Java Memory Model for Programmers
Introduction
Discussions of multithreaded programming and the Java Memory Model often get bogged down in discussion of complex compiler optimizations. I don’t think it needs to get that complicated—if all you want to do is write correct multithreaded programs, there’s a pretty simple mental model you can follow.
How to Think about Memory
Each thread has its own view of memory such that two threads can disagree on the contents of the same field. Writes made to a field by one thread may bleed through and become visible to others, but they may also appear only transiently, in the wrong order (even before they are “supposed” to happen!), or not at all. This is permitted for performance reasons.
However, Java does provide a few specific ways for threads to reliably communicate writes to one another…
The Tools at Your Disposal
There are five ways to explicitly ensure that one thread’s writes up to a certain point will be visible (and in the right order!) to other threads. Each one uses a particular sort of language primitive as a rendezvous for a writing thread to “publish” its writes, and for other threads to “receive” them:
| Primitive | Writes up to and including… | ...are made visible to… |
|---|---|---|
| Object | the end of a synchronized block or method | a thread entering a synchronized block or method for the same object. |
| Volatile field | a write to a volatile field | any thread reading that volatile field. |
| Thread | a call to Thread.start |
the newly started thread. |
| Thread | the final write made by a dying thread | any thread which successfully calls Thread.join on that thread. |
| Final field | the initialization of a final field (but only those writes affecting the field and any object it references) | any thread, provided that the constructor of the object containing the field doesn’t write the value of this anywhere eventually visible to other threads |
Note that for the rendezvous to happen, it must be the same thread, the same field, or the same monitor on both the “publishing” and “receiving” sides.
Really, at all levels of abstraction, all correct concurrent programs share
a particular characteristic: threads do not communicate with each other
at all except when they mutually agree to communicate at common rendezvous
points. Any truly unilateral attempt at communication (like
Thread.suspend) is inherently unsafe.
Applying Your Knowledge
Let’s say we’ve got a class like this:
class Counter {
private int count = 0;
public synchronized void increment() {
count++;
}
public synchronized int getCount() {
return count;
}
}
Making increment synchronized ensures not only that the increment of
count (which involves a read and then a write of the modified value) isn’t
disturbed, it also ensures that the modified value of count is “published”
where other threads can potentially see it.
Likewise, making getCount synchronized ensures that the thread “receives”
any “published” changes to the value of count when the method body is
entered.
Both synchronized declarations are necessary for this code to work as
expected. Of course, since getCount is simply reading a single int
field (which is already an atomic operation), there is another way to
approach this:
class Counter {
private volatile int count = 0;
public synchronized void increment() {
count++;
}
public int getCount() {
return count;
}
}
This means that increment will “publish” twice: once when it writes to
count, and once when it exits the synchronized method. This has no ill
effects and only a very minor peformance penalty.
getCount only “receives” once: when it reads count. Since it is not
synchronized by a monitor, concurrent calls for getCount do not have to
wait for each other to complete. This can result in improved performance
for readers.
Note that, had increment performed any writes to other fields after the
write to count, those writes would not be safely visible to callers of
getCount; the second “publication” which happens upon existing the
synchronized method is via the object’s monitor, not the volatile field.
The Consequences
If you do not properly coordinate the “publication” and “reception” of writes between threads that share fields, then your program will fail transiently. It is more likely to do so under production load on high-end hardware than it is during testing on your development machine.
You don’t want that. Neither does your boss.
Further Reading
The JSR 133 FAQ goes into more detail about the memory model itself. Like the Memory Model document, it goes a little more into the effects of specific kinds of optimizations.
One Last Thing
If you ever do get dragged into a discussion of compiler and runtime optimizations, it’s important to remember several things:
- Disassembling
.classfiles doesn’t tell you much about what will happen at runtime:javacperforms very few optimizations, leaving most of them up to the interpreter and JIT compiler. - While the interpreter and the JIT compiler are prohibited from performing certain optimizations like reordering dependent reads or writes, the CPU itself is not.
- A few CPU architectures can and will reorder dependent memory operations and get away with it—unless you are writing improperly synchronized programs.
- Still, today’s hardware is often fairly forgiving about multithreading. As we’ve run out of room to scale “up” and have to scale “out” instead, future hardware will have to sacrifice that forgiveness for increased concurrent performance. Unless you want to rewrite all your code in five years, do it right the first time.
One Other Thing
Try using the classes in java.util.concurrent and
java.util.concurrent.atomic before rolling your own custom solutions
using synchronzied and volatile. The java.util.concurrent classes
are likely to be both correct and as fast as possible while still maintaining
correctness (which is seldom easy to achieve). They’re built on top
of the basic primitives themselves, so they have similar characteristics:
for example, writing to a java.util.concurrent queue “publishes” to the
queue’s readers.