Introduction to the Java Memory Model

The Java Memory Model (JMM) describes the behavior of threads in the Java runtime environment. The memory model is part of the semantics of the Java language, and describes what a programmer can and should not expect when developing software not for a specific Java machine, but for Java as a whole.

The original Java memory model (which, in particular, refers to “percolocal memory”), developed in 1995, is considered a failure: many optimizations cannot be made without losing the guarantee of code safety. In particular, there are several options to write multi-threaded "single":

  • either every act of accessing a singleton (even when the object was created a long time ago, and nothing can change) will cause an inter-thread lock;
  • or under a certain set of circumstances, the system will issue an unfinished loner;
  • or under a certain set of circumstances, the system will create two loners;
  • or the design will depend on the behavior of a particular machine.

Therefore, the memory mechanism has been redesigned. In 2005, with the release of Java 5, a new approach was presented, which was further improved with the release of Java 14.

The new model is based on three rules:

Rule #1 : Single-threaded programs run pseudo-sequentially. This means: in reality, the processor can perform several operations per clock, at the same time changing their order, however, all data dependencies remain, so the behavior does not differ from sequential.

Rule number 2 : there are no out of nowhere values. Reading any variable (except non-volatile long and double, for which this rule may not hold) will return either the default value (zero) or something written there by another command.

And rule number 3 : the rest of the events are executed in order, if they are connected by a strict partial order relationship “executes before” ( happens before ).

Happens before

Leslie Lamport came up with the concept of Happens before . This is a strict partial order relation introduced between atomic commands (++ and -- are not atomic) and does not mean "physically before".

It says that the second team will be "in the know" of the changes made by the first.

Happens before

For example, one is executed before the other for such operations:

Synchronization and monitors:

  • Capturing the monitor ( lock method , synchronized start) and whatever happens on the same thread after it.
  • The return of the monitor (method unlock , end of synchronized) and whatever happens on the same thread before it.
  • Returning the monitor and then capturing it by another thread.

Writing and reading:

  • Writing to any variable and then reading it in the same stream.
  • Everything in the same thread before writing to the volatile variable, and the writing itself. volatile read and everything on the same thread after it.
  • Writing to a volatile variable and then reading it again. A volatile write interacts with memory in the same way as a monitor return, while a read is like a capture. It turns out that if one thread wrote to a volatile variable, and the second found it, everything that precedes the write is executed before everything that comes after the read; see picture.

Object maintenance:

  • Static initialization and any actions with any instances of objects.
  • Writing to final fields in the constructor and everything after the constructor. As an exception, the happens-before relation does not transitively connect to other rules and can therefore cause an inter-thread race.
  • Any work with the object and finalize() .

Stream service:

  • Starting a thread and any code in the thread.
  • Zeroing variables related to the thread and any code in the thread.
  • Code in thread and join() ; code in the thread and isAlive() == false .
  • interrupt() the thread and detect that it has stopped.

Happens before work nuances

Releasing a happens-before monitor occurs before acquiring the same monitor. It is worth noting that it is the release, and not the exit, that is, you don’t have to worry about safety when using wait.

Let's see how this knowledge will help us correct our example. In this case, everything is very simple: just remove the external check and leave the synchronization as it is. Now the second thread is guaranteed to see all the changes, because it will only get the monitor after the other thread releases it. And since he will not release it until everything is initialized, we will see all the changes at once, and not separately:

public class Keeper {
    private Data data = null;

    public Data getData() {
        synchronized(this) {
            if(data == null) {
                data = new Data();
            }
        }

        return data;
    }
}

Writing to a volatile variable happens-before reading from the same variable. The change we've made, of course, fixes the bug, but it puts whoever wrote the original code back where it came from - blocking every time. The volatile keyword can save. In fact, the statement in question means that when reading everything that is declared volatile, we will always get the actual value.

In addition, as I said earlier, for volatile fields, writing is always (including long and double) an atomic operation. Another important point: if you have a volatile entity that has references to other entities (for example, an array, List or some other class), then only a reference to the entity itself will always be “fresh”, but not to everything in it incoming.

So, back to our Double-locking rams. Using volatile, you can fix the situation like this:

public class Keeper {
    private volatile Data data = null;

    public Data getData() {
        if(data == null) {
            synchronized(this) {
                if(data == null) {
                    data = new Data();
                }
            }
        }
        return data;
    }
}

Here we still have a lock, but only if data == null. We filter out the remaining cases using volatile read. Correctness is ensured by the fact that volatile store happens-before volatile read, and all operations that occur in the constructor are visible to whoever reads the value of the field.