An Introduction to the Java Memory Model

Prashant Pandey
Jul 3, 2020 · 6 min read

This is a short article to give you a taste of the Java Memory Model. Being unaware of its rules can lead to some pretty knotty bugs. Any serious Java developer who wishes to write thread-safe parallel programs should be aware of the memory visibility issues that arise due to the JMM.

What Is a Memory Model?

Any modern computer architecture can do such things like cache global variables and reorder instructions to get a performance boost. It is not guaranteed that the instructions are run in the order as they were written in the high-level program. In a single threaded program, the execution follows the as-if-serial semantics. That is, while the instructions can be reordered, the result would be as if the program were executed serially. This does not hold true for concurrent programs.

Why the Java Memory Model?

Since each architecture can have its own memory model, the JMM abstracts all of them out by providing a common memory model on any architecture. The programmer does not have to worry himself about the underlying architecture, as long as he/she is following the rules of the JMM.

Total Order vs. Partial Order

Informally speaking, a total order is a binary relation that applies to any two elements of a set. For instance, for the set of integers, we have a total-order less-than. Any you items in the set can be are related through this relation.

A partial-order however, applies to only some of the elements of the set. An example of this would be divisible-by. Take 2 and 3 for instance. Neither 2 is divisible-by 3 nor 3 is divisible-by 2. This relation applies to only a subset of elements, and therefore, is a partial-order. In this case, we have 4 is divisible-by 2.

For a partial-order, the binary relation is transitive (this is the only property you will need to understand this article, so I skip the rest). That is, a rel b and b rel c, then a rel c.

Details

The Java Memory Model (JMM) guarantees a as-if-serial guarantee to the execution of a program iff there is a happens-before relationship b/w the instructions. While the compiler can still reorder instructions that have a happens-before relationship, the final result will be as if the entire program were executed serially.

The JMM guarantees that instructions executing within a thread have a happens-before relationship b/w them. This guarantees that if the thread writes to a variable and then reads it, it will see the last written value. Another happens-before guarantee that the JMM gives is that a monitor lock can be acquired only after it has been relinquished (regardless of what thread relinquishes it and what thread acquires it).

What distinguishes a parallel program from a serial program is that the instructions in the latter are totally-ordered (governed by happens-before), while the former only has a partial-order. A parallel program can be given a total order with proper synchronisation. The more ordering we can ensure, the better we can reason about the behaviour of the program. Now consider the following implementation of lazy loading:

@NotThreadSafe
public class
LazyObjectFactory {

private LazyObject instance;

public LazyObject getInstance() {
if(null == instance) { //read1
instance = new LazyObject();
}
return instance; //read2
}
}

The above program basically is a factory that maintains a singleton. This class is obviously not thread safe as there is a race-condition. It’s possible that the correct behaviour of the program is broken in case two threads call getInstance() in parallel, leading to the creation of two LazyObject instances.

But there is something much more sinister going on here. The above code can show some pretty absurd behaviour:

  1. Thread A initialises the object and assigns it to instance. However, thread B still see it as null and goes ahead with creating another instance.
  2. Thread A initialised the object and assigns it to instance. Thread B sees a non-null value during read1, but returns an improperly constructed object.

For the initialising thread, this relation satisfies:

But observe that no such relationship exists b/w the assignment in thread A and the read in thread B, leading to 1.

To see how 2 takes place, note that thread B can observe thread A’s actions in the opposite order. That is, the assignment takes place before the initialisation. So read1 shows a non-null value, but an improperly constructed object is returned.

Now consider the thread-safe version of the above program:

@ThreasSafe
public class
LazyObjectFactory {

private LazyObject instance;

public synchronised LazyObject getInstance() {
if(null == instance) { //read1
instance = new LazyObject();
}
return instance; //read2
}
}

How does synchronisation help us? The following diagram illustrates it:

We know that the JMM guarantees a happens-before relation b/w a lock’s release and its acquisition (release happens-before acquisition). The lock release happens-before lock acquire, we have a total ordering across events for these two threads (whatever thread A did before relinquishing the lock and whatever thread B did after acquiring the lock). This alleviates both of the problems above.

The diagram above illustrates an important concept. Since we have a total ordering, any memory writes that thread A did before relinquishing the lock will be visible to thread B, and not just the proper publishing of instance.

The JMM also guarantees a happens-before relationship b/w writing to a volatile variable and reading from it. That is, any subsequent reads from any thread will see the latest value written from any thread. Note that volatile variables are not a replacement for proper locking. For instance, declaring count as volatile does not guarantee that count++ will be atomic. You have to encapsulate it in synchronised blocked as it is a compound action that needs to be executed atomically. All volatile ensures is proper memory visibility.

Static Initialisers and Thread Safety

An easy way to create thread-safe singleton factories is using static initialisers as:

@ThreadSafe
public class
SingletonFactory {

private static Singleton instance = new Singleton();

public Singleton getInstance() {
return instance;
}
}

Since this is a static initialiser, it will be called once the class is loaded (and before any thread can start using this class). The object will be properly constructed and the reference properly initialised before any other thread and use it. There is again a happens-before relationship that guarantees that no such issues as above arise.

The above implementation can be used to create objects lazily in a thread-safe manner as:

@ThreadSafe
public class
SingletonFactory {

public static Singleton getInstance() {
return Creator.instance;
}
private static class Creator {
private static Singleton instance = new Singleton();
}
}

Creator is loaded lazily when the first call to getInstance() takes place. This achieves lazy-initialisation in a thread safe manner.

Double Checked Locking

Now that you are a bit aware of the JMM, you should be able to tell why the code below is not thread safe:

@NotThreadSafe
public class
SingletonFactory {

private Singleton instance;

public Singleton getInstance() {
if(null == instance) {
synchronized (this) {
if(null == instance) {
instance = new Singleton();
}
}
}
return instance;
}

}

This technique is called Double Checked Locking and aims to optimise the lock contention by first checking if the instance is null. But there is a fatal flaw, can you see it?

This article is meant to be a gentle introduction to the JMM. It’s too vast a topic to be covered in a single article. You should at least be aware of these basic to avoid making any catastrophic mistakes writing parallel programs.

References:

Java: Concurrency in Practice by Brian Goetz

The Startup

Get smarter at building your thing. Join The Startup’s +800K followers.

Prashant Pandey

Written by

SO: https://stackoverflow.com/users/6714426/prashant-pandey

The Startup

Get smarter at building your thing. Follow to join The Startup’s +8 million monthly readers & +800K followers.

Prashant Pandey

Written by

SO: https://stackoverflow.com/users/6714426/prashant-pandey

The Startup

Get smarter at building your thing. Follow to join The Startup’s +8 million monthly readers & +800K followers.

Medium is an open platform where 170 million readers come to find insightful and dynamic thinking. Here, expert and undiscovered voices alike dive into the heart of any topic and bring new ideas to the surface. Learn more

Follow the writers, publications, and topics that matter to you, and you’ll see them on your homepage and in your inbox. Explore

If you have a story to tell, knowledge to share, or a perspective to offer — welcome home. It’s easy and free to post your thinking on any topic. Write on Medium

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store