The JMM gives us certain guarantees about ordering, which we have to know/use in order to avoid data-races.

Note that the JMM itself builds upon the guarantees given by our processor architecture:

The JVM has to consider these constraints when compiling our code.

7.1 Program Order

Program Order (PO)

An order within a thread. It doesn’t guarantee memory access order across threads. Its main purpose is to link the execution trace back to the original source code.

JMM Intra-thread guarantee

The JMM guarantees us that intra-thread everything appears as if it was executed sequentially! (PO)

7.2 Synchronisation Actions

Synchronisation actions are a set of specific actions, as designated by the JMM, which guarantee the order of execution.

Synchronization Actions (SA)

Special actions that create ordering constraints. Includes reads/writes to volatile variables, lock/unlock operations, thread start/join, etc.

Synchronization Order (SO)

A global total order agreed upon by all threads for all synchronization actions.

The set of synchronization actions

  1. Volatile reads and volatile writes.
  2. Lock and unlock actions on a monitor (entering/exiting a synchronized block or method, or explicit Lock.lock() / unlock()).
  3. The action that starts a thread (Thread.start()) and the action that detects a thread has terminated (a successful Thread.join(), or isAlive() returning false).
    1. .start() has to happen before every single action in that thread
  4. The initial action in a thread and the final action in a thread.
  5. The default initialization of an object’s fields (conceptually a write of default values that happens at construction).
  6. External actions — I/O interactions with the “outside world” that the JMM can’t see through.

LEMMA

Synchronisation actions themselves do not guarantee a specific ordering. Synchronizes with (SW) only pairs the specific actions which “see” each other

7.2.1 Volatile Keyword

Volatile is a keyword that ensures that a thread accessing the variable sees the most recent version of it.

When you read a volatile variable in a thread, all writes up to and including the last write to that volatile are now visible to your thread!

It’s basically a flag to the compiler to not optimise that field away as it’s shared.

In this example, marking x as volatile would fix the issue.

Note? Having everything inside a synchronized block gives us the same properties as the volatile does. unlocking/locking monitor is also a synchronization action.

7.3 Happens-Before Closure

Example: In example, because volatile introduces a synchronisation order we get the following relationships:

  1. x = 1 < int r1 = y (PO)
  2. y = 1 < int r2 = x (PO)

can never happen because:

  1. Assume r1 = y < y = 1 and r2 = x < x = 1
  2. Then because of PO we get:

which forms a cycle and thus a contradiction, as is impossible.

The possible outcomes are , and , depending on which ordering we choose. It’s non-deterministic.

Inside the processor

Volatile prevents StoreLoad Reordering this is what ensures that the above example does not fail!

Without it, the CPU would be free to reorder the writes and loads there.

7.3.1 Inconsistent orderings

Case 4 is not possible here:

  1. if r1 = g reads g = 1, this means that x = 1 must have already executed (PO)
  2. Thus we cannot read r2 = x where x = 0!

7.3.2 Transitive closure

We can see, if we pick a sw synchronisation order, we can derive the happens-before relationships from the transitive closure.

7.3.3 Data Races Allowed

JMM’s happens-before consistency rule (JLS §17.4.6) — the actual definition of which values a read is allowed to return.
Stated precisely: a read of variable may observe a write (to ) exactly when:

  1. does not happen-before (no reading from the future)
  2. there is no write (to ) with (no write strictly between and in hb)

Why “races are allowed”. A data race is defined as two conflicting accesses (one of them a write) on the same variable, hb-unordered.

  • The rule above says a read may observe any hb-unordered write to its variable. So racy programs aren’t forbidden — they’re given defined, if non-deterministic, semantics.

This explains why for Dekker’s, we can observe :

int x = 0, y = 0; // plain 
// Thread 1: 
x = 1; int r1 = y; 
// Thread 2: 
y = 1; int r2 = x;

because no read and write order exists, we may observe any unordered write!