The JMM specifies the minimal guarantees the JVM must make about when writes to variables become visible to other threads. It was designed to balance the need for predictability and ease of program development with the realities of implementing high-performance JVMs on a wide range of popular processor architectures.
In a shared-memory multiprocessor architecture, each processor has its own cache that is periodically reconciled with main memory. Processor architectures provide varying degrees of cache coherence; some provide minimal guarantees that allow different processors to see different values for the same memory location at virtually any time.
An architecture’s memory model tells programs what guarantees they can expect from the memory system, and specifies the special instructions required (called memory barriers or fences) to get the additional memory coordination guarantees required when sharing data.
In order to shield the Java developer from the differences between memory models across architectures, Java provides its own memory model, and the JVM deals with the differences between the JMM and the underlying platform’s memory model by inserting memory barriers at the appropriate places.
he various reasons why operations might be delayed or appear to execute out of order can all be grouped into the general category of reordering.
The JMM defines a partial ordering called happens-before on all actions within the program. To guarantee that the thread executing action B can see the results of action A (whether or not A and B occur in different threads), there must be a happens-before relationship between A and B. In the absence of a happens-before ordering between two operations, the JVM is free to reorder them as it pleases.
A data race occurs when a variable is read by more than one thread, and written by at least one thread, but the reads and writes are not ordered by happens-before. A correctly synchronized program is one with no data races; correctly synchronized programs exhibit sequential consistency, meaning that all actions within the program appear to happen in a fixed, global order.
The rules for happens-before are:
- Program order rule. Each action in a thread happens-before every action in that thread that comes later in the program order.
- Monitor lock rule. An unlock on a monitor lock happens-before every subsequent lock on that same monitor lock.
- Volatile variable rule. A write to a volatile field happens-before every subsequent read of that same field.
- Thread start rule. A call to
Thread.start
on a thread happens-before every action in the started thread. - Thread termination rule. Any action in a thread happens-before any other thread detects that thread has
terminated, either by successfully return from
Thread.join
or byThread.isAlive
returningfalse
. - Interruption rule. A thread calling interrupt on another thread happens-before the interrupted thread detects
the interrupt (either by having
InterruptedException
thrown, or invokingisInterrupted
orinterrupted
). - Finalizer rule. The end of a constructor for an object happens-before the start of the finalizer for that object.
- Transitivity. If
A
happens-beforeB
, andB
happens-beforeC
, thenA
happens-beforeC
.
When two threads synchronize on different locks, we can’t say anything about the ordering of actions between them—there is no happens-before relation between the actions in the two threads.
Because of the strength of the happens-before ordering, you can sometimes piggyback on the visibility properties of an existing synchronization. This entails combining the program order rule for happens-before with one of the other ordering rules (usually the monitor lock or volatile variable rule) to order accesses to a variable not otherwise guarded by a lock. This technique is very sensitive to the order in which statements occur and is therefore quite fragile.
This technique is called “piggybacking” because it uses an existing happens-before ordering that was created for some
other reason to ensure the visibility of object X
, rather than creating a happens-before ordering specifically for
publishing X
.
Some happens-before orderings guaranteed by the class library include:
- Placing an item in a thread-safe collection happens-before another thread retrieves that item from the collection;
- Counting down on a
CountDownLatch
happens-before a thread returns fromawait
on that latch; - Releasing a permit to a
Semaphore
happens-before acquiring a permit from that sameSemaphore
; - Actions taken by the task represented by a
Future
happens-before another thread successfully returns fromFuture.get
; - Submitting a
Runnable
orCallable
to anExecutor
happens-before the task begins execution; and - A thread arriving at a
CyclicBarrier
orExchanger
happens-before the other threads are released from that same barrier or exchange point. IfCyclicBarrier
uses a barrier action, arriving at the barrier happens-before the barrier action, which in turn happens-before threads are released from the barrier.
With the exception of immutable objects, it is not safe to use an object that has been initialized by another thread unless the publication happens-before the consuming thread uses it.
Static initializers are run by the JVM at class initialization time, after class loading but before the class is used by any thread. Because the JVM acquires a lock during initialization and this lock is acquired by each thread at least once to ensure that the class has been loaded, memory writes made during static initialization are automatically visible to all threads. Thus statically initialized objects require no explicit synchronization either during construction or when being referenced. However, this applies only to the as-constructed state—if the object is mutable, synchronization is still required by both readers and writers to make subsequent modifications visible and to avoid data corruption.
Lazy initialization holder class idiom:
@ThreadSafe
public class ResourceFactory {
private static class ResourceHolder {
static Resource resource = new Resource();
}
public static Resource getResource() {
return ResourceHolder.resource;
}
}
The purpose of double-checked locking (DCL) was to reduce the impact of synchronization while implementing lazy
initialization in earlier Java versions. The way it worked was first to check whether initialization was needed without
synchronizing, and if the resource reference was not null
, use it. Otherwise, synchronize and check again if
the Resource
is initialized, ensuring that only one thread actually initializes the shared Resource
. The common code
path—fetching a reference to an already constructed Resource — doesn't use synchronization. And that’s where the problem
is: it is possible for a thread to see a partially constructed Resource
.
Subsequent changes in the JMM (Java 5.0 and later) have enabled DCL to work if resource is made volatile
, and the
performance impact of this is small since volatile reads are usually only slightly more expensive than nonvolatile
reads.
However, this is an idiom whose utility has largely passed—the forces that motivated it (slow uncontended synchronization, slow JVM startup) are no longer in play, making it less effective as an optimization. The lazy initialization holder idiom offers the same benefits and is easier to understand.
The guarantee of initialization safety allows properly constructed immutable objects to be safely shared across threads without synchronization, regardless of how they are published—even if published using a data race.
Initialization safety guarantees that for properly constructed objects, all threads will see the correct values of final fields that were set by the constructor, regardless of how the object is published. Further, any variables that can be reached through a final field of a properly constructed object (such as the elements of a final array or the contents of a HashMap referenced by a final field) are also guaranteed to be visible to other threads.
Initialization safety makes visibility guarantees only for the values that are reachable through final fields as of the time the constructor finishes. For values reachable through non-final fields, or values that may change after construction, you must use synchronization to ensure visibility.
The Java Memory Model specifies when the actions of one thread on memory are guaranteed to be visible to another. The
specifics involve ensuring that operations are ordered by a partial ordering called happens-before, which is specified
at the level of individual memory and synchronization operations. In the absence of sufficient synchronization, some
very strange things can happen when threads access shared data. However, the higher-level rules offered in Chapters 2
and 3, such as @GuardedBy
and safe publication, can be used to ensure thread safety without resorting to the low-level
details of happens-before.