The Java Memory Model (JMM) Quiz
KOTLIN › Concurrency
Thread B loops on a plain Boolean and never exits, even though thread A set the flag seconds ago. What does the JMM say about this?
- It's legal: without a happens-before edge, B may read its cached false forever
- It's a JVM bug: plain writes must propagate within a bounded time
- It can only happen if A and B run on different CPU sockets
- The loop prevents the write: reads always win over pending writes
Answer: It's legal: without a happens-before edge, B may read its cached false forever
No synchronisation means no visibility guarantee, ever. The JIT may even hoist the read out of the loop entirely. A volatile flag (or any lock/atomic protecting it) creates the edge that forces B to observe the write.
What exactly does the Java Memory Model define?
- When one thread's writes are guaranteed to be visible to another thread
- How the heap is divided between threads and the GC
- The scheduling order in which runnable threads execute
- How many bytes each primitive type occupies in memory
Answer: When one thread's writes are guaranteed to be visible to another thread
The JMM is a visibility-and-ordering rulebook, not a scheduler or memory layout spec. It defines which executions (with caching and reordering) are legal, and which synchronisation actions force writes to be seen.
Two threads run count++ 1000 times each on a @Volatile int. The result is 1400. Why?
- count++ is read-add-write; volatile fixes visibility, not the interleaving
- Volatile writes are buffered and 600 were dropped on contention
- The JIT optimised away 600 redundant increments
- Int overflow wrapped the counter past its maximum
Answer: count++ is read-add-write; volatile fixes visibility, not the interleaving
Both threads can read the same value, both add one, and both store the same result: one increment vanishes. Each individual read and write was perfectly visible; the compound operation was never atomic. This is THE most common concurrency gotcha.
Which is the correct fix for that lost-increment race?
- AtomicInteger.incrementAndGet(), or a lock around the whole increment
- Declare the counter @Volatile AND read it twice before writing
- Yield the thread between the read and the write
- Store the counter in a HashMap keyed by thread id
Answer: AtomicInteger.incrementAndGet(), or a lock around the whole increment
The read-modify-write must execute as one indivisible step: AtomicInteger does it with a CAS loop, a lock does it with mutual exclusion. Double-reading or yielding just reshuffles the interleavings.
A thread reads a non-null instance whose fields are still zero/default. What made this possible?
- The reference store was reordered before the constructor's field stores
- The constructor threw and the JVM published the partial object
- The reader thread has a stale copy of the fields but a fresh reference
- The object was moved by the GC mid-construction
Answer: The reference store was reordered before the constructor's field stores
The compiler/CPU may reorder independent stores: publish the reference first, initialise fields after. Another thread wins the race and dereferences a half-built object. This is the ordering problem, distinct from visibility and atomicity.
Which set of tools forbids that half-built-object publication?
- A volatile reference, synchronized access, or final fields
- Thread.sleep after construction, then publishing
- Making the constructor private and using a factory
- Assigning the reference twice so the second write wins
Answer: A volatile reference, synchronized access, or final fields
Volatile ordering semantics forbid moving the field stores past the reference store; a lock brackets construction and publication; final fields get a freeze at the end of construction that guarantees initialised values to all readers.
If action A happens-before action B, what is guaranteed?
- A's writes are visible to B, and A is ordered before B
- A and B execute on the same thread
- B cannot start until A's thread terminates
- A and B are both atomic with respect to each other
Answer: A's writes are visible to B, and A is ordered before B
That is the entire contract: visibility plus ordering. It says nothing about which thread runs what or about atomicity. All the sync tools (volatile, monitors, atomics, thread start/join) are just different ways to create these edges.
Which pair forms a happens-before edge?
- A write to a volatile field, then a later read of that same field
- Two consecutive reads of the same volatile field
- A plain field write, then a Thread.sleep on the reader
- Two synchronized blocks on two different monitors
Answer: A write to a volatile field, then a later read of that same field
Volatile write then read of the SAME field is a canonical edge, and it publishes everything the writer did before the write (not just that field). Different monitors create no edge between each other, and sleep has no memory semantics.
Beyond mutual exclusion, what memory guarantee does synchronized give?
- Unlock happens-before the next lock of the same monitor, so the next holder sees all prior writes
- All fields touched in the block become volatile afterwards
- Writes inside the block flush only when the thread ends
- It guarantees visibility only for the fields of the monitor object
Answer: Unlock happens-before the next lock of the same monitor, so the next holder sees all prior writes
Release then acquire of the same monitor is a happens-before edge covering everything the previous holder wrote, any fields anywhere. That's why consistently locking on one monitor needs no volatile fields at all.
What does Thread.join() guarantee to the caller when it returns?
- Every write the joined thread made is now visible
- Only the joined thread's final return value is visible
- Nothing about memory: join only waits for termination
- Visibility for volatile fields the thread wrote, but not plain ones
Answer: Every write the joined thread made is now visible
Everything in a thread happens-before another thread successfully returning from join() on it, so all its writes (plain or volatile) are visible afterwards. start() is the mirror edge into the thread.
Why are immutable objects (all fields final/val, no escape during construction) thread-safe with zero synchronisation?
- Final fields are frozen at construction: every thread sees them fully initialised
- The JVM copies immutable objects into each thread's stack
- The GC synchronises threads whenever it scans such objects
- They are not: readers still need a volatile reference in all cases
Answer: Final fields are frozen at construction: every thread sees them fully initialised
The JMM gives final fields special semantics: after safe construction, any thread that obtains the reference sees the final fields correctly initialised, even via a data race. State that never changes cannot be seen inconsistently.
What does 'ConcurrentHashMap is thread-safe' precisely mean in JMM terms?
- Each operation is atomic and creates happens-before edges, but your own check-then-act sequences still race
- Any sequence of calls on it is automatically atomic
- It snapshots the map for every reader thread
- It makes the values you store in it immutable
Answer: Each operation is atomic and creates happens-before edges, but your own check-then-act sequences still race
The map hand-rolls the JMM machinery internally, so put/get are individually correct and visible. Compound logic (if absent then put) is YOUR race unless you use the atomic compound ops: putIfAbsent, compute, merge.
In Kotlin, val config by lazy { load() } is read by many threads. Is initialisation safe?
- Yes: lazy defaults to SYNCHRONIZED mode, so one thread initialises and all see the result
- No: lazy must be wrapped in a volatile reference manually
- Only if every reader also declares the property @Volatile
- Only on the JVM if the value type has no mutable fields
Answer: Yes: lazy defaults to SYNCHRONIZED mode, so one thread initialises and all see the result
LazyThreadSafetyMode.SYNCHRONIZED is the default: initialisation runs once under a lock and publication rides the monitor's happens-before edge. PUBLICATION allows racing initialisers with one winner; NONE is single-thread only.
Two coroutines on Dispatchers.Default increment a plain var 1000 times each via launch. What is true?
- It races exactly like threads: coroutines on a multi-threaded dispatcher share the JMM's rules
- It's safe: the coroutine machinery serialises all state access
- It's safe if both use the same CoroutineScope
- It only races if the var is captured by reference
Answer: It races exactly like threads: coroutines on a multi-threaded dispatcher share the JMM's rules
Coroutines are not a memory model: on a multi-threaded dispatcher they hit the same visibility/atomicity rules. Fixes are the same spirit: confine state to one thread/dispatcher, use atomics, or a Mutex. (Suspension points do create happens-before edges, which is why confined code is safe.)