Digital Garden
Computer Science
Concurrent & Parallel Programming
JMM - Java Memory Model

JMM - Java Memory Model

The Java Memory Model (JMM) specifies guarantees that are given by the Java Virtual Machine (JVM) relating to concurrency:

  • When writing operations on variables become visible to other threads
  • Which operations are atomic
  • Ordering of operations, meaning under which circumstances can the effects of operations appear out of order to any given thread.

Memory layout

Modern CPUs don't just work with the main memory (RAM) they also have multiple layers of caches and registers to perform more efficiently. You can see on this page (opens in a new tab) why it is worth having these caches and the difference in the time it takes to read depending on how far down the CPU has to reach for the data. However, this means that there can be multiple versions of the same data on different levels which can lead to issues. Additionally, as we know all threads share the main memory however each core and therefore thread has its own cache levels so there can be inconsistency inside a thread but also between threads.

To illustrate this we have the program below. When running the program we expect to see the values (1,0), (1,1) and (0,1) for all 6 possible interleavings.

However, when running the program we also get (0,0). This is due to either compiler reordering or caching.

Happens before rules

The JMM defines a relationship called happens-before on actions such as reading/writing to variables, locking/releasing monitors and starting/joining threads. These happens-before relationships guarantee that a thread executing action A can see the results of action B on the same or a different thread. If there is no such relationship then there is no guarantee!

Rule 1

Each action in a thread happens-before every action in that thread that comes later in the program order.

Rule 2

Releasing a lock happens-before every subsequent lock on the same lock.

Rule 3

A write to a volatile field happens-before every subsequent read of the same field.

Rule 4

A call to start a thread with start() happens-before every subsequent action in the started thread.

Rule 5

Actions in a thread t1 happens-before another thread detects the termination of thread t1.

Rule 6

The happens-before order is transitive.

Volatile

Volatile fields guarantee the visibility of writes (i.e. volatile variables are never cached). Read access to a volatile field implies getting fresh values from memory (slower). Write access to a volatile field forces the thread to flush all pending writes to the memory level. Volatile variables have a cost due to these things having to be done and caching no longer being allowed. Important to note is also that access to a volatile variable inside a loop can be more expensive than synchronizing the entire loop.

class MyExchanger {
    private volatile Pair data = null;
    public String getPairAsString() {
        return data == null ? null : data.toString();
    }
    public boolean isReady() {
        return data != null;
    }
    public void setPair(Object first, Object second) {
        Pair tmp = new Pair();
        tmp.setFirst(first);
        tmp.setSecond(second);
        data = tmp; // guaranteed to have both
    }
}

Fixing Assignment Atomicity

Depending on the implementation a long or double assignment double x = 3; is not atomic, it will most lightly write 32 bits at a time. To prevent this we can make the double volatile which will guarantee the assignment to be atomic.

Double-checked Locking Problem

We want a Singleton that has lazy initialization that is also thread-safe. Our first attempt could be something like the code below with the getInstance() function being synchronized so that we don't run into problems. And this works fine however it is very expensive because for every getInstance we have the synchronization overhead.

public class Singleton {
    private static Singleton instance;
    public synchronized static Singleton getInstance() {
        if(instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
    private Singleton() { /* initialization */ }
}

To fix this we need to do so-called double-checking. We also need to make the instance volatile to prevent there being uninitialized objects.

public class Singleton {
    private volatile static Singleton instance;
    public static Singleton getInstance() {
        if(instance == null) {
            synchronized(Singleton.class) {
                if(instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
    private Singleton() { /* initialization */ }
    // other methods
}