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).
  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.

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.

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!