Understanding the Java Memory Model: The Invisible Rules Behind Concurrent Java Programs
Modern software rarely runs on a single thread anymore. From backend systems handling thousands of requests to financial platforms processing transactions in real time, concurrency has become a core part of software engineering. But with concurrency comes complexity — especially when multiple threads interact with shared memory.
At a recent Naija Java User Group (NaijaJUG) Lightning Talks session, Victor Adepoju, a Software Engineer at Moniepoint, delivered a practical session on one of Java’s most misunderstood but essential topics: The Java Memory Model (JMM).
The session explored how Java handles memory visibility, instruction ordering, and synchronization in multithreaded applications — and why understanding these rules is critical for writing correct concurrent programs.
Why Does Java Need a Memory Model?
When writing single-threaded programs, execution feels predictable: line 1 runs before line 2, and everything behaves exactly as written.
Multithreaded programs are different.
Modern CPUs optimize performance aggressively. They:
- Cache values locally
- Reorder instructions
- Delay writes to shared memory
These optimizations are great for speed but can create unexpected behavior when threads share data.
Imagine two threads running on separate CPU cores.
Thread A updates a variable.
Thread B reads that same variable.
You might expect Thread B to immediately see Thread A’s update — but that isn’t always true.
Why?
Because Thread B might still be reading an old cached copy instead of the latest value in main memory.
That’s where the Java Memory Model comes in.
The JMM defines the rules that govern:
- When writes become visible to other threads
- What instruction reorderings are allowed
- How synchronization guarantees correctness
Think of it as Java’s contract for safe concurrency.
Instruction Reordering: The Hidden Optimization
One of the most surprising concepts in concurrent programming is instruction reordering.
Compilers and processors may reorder operations to improve performance — as long as the result appears unchanged in a single-threaded context.
Example:
int a = 5;
int b = 10;
int c = a + b;
System.out.println(c);The JVM might internally reorder the first two assignments:
int b = 10;
int a = 5;Since the final result remains the same, this optimization is legal.
But in multithreaded systems, reordering can produce strange outcomes.
Victor demonstrated this with a classic concurrency example involving two threads updating and reading shared variables.
Possible outputs included:
(1, 0)(0, 1)(1, 1)- Even
(0, 0)
That last outcome surprises most developers.
How can both threads read zero after both wrote one?
Because without proper synchronization, the JMM allows reads to happen before writes become visible due to caching, scheduling, or reordering.
This is exactly why concurrent programming can feel unpredictable.
The Three Core Guarantees of the JMM
The Java Memory Model helps us reason about concurrency through three guarantees.
1. Visibility
Can one thread see changes made by another thread?
Without synchronization, the answer may be no.
A write may stay trapped in CPU cache indefinitely.
2. Atomicity
Does an operation happen as a single indivisible action?
Example:
count++;Looks simple, but it actually involves:
- Read value
- Increment
- Write value
That means count++ is not atomic.
Multiple threads performing this simultaneously can overwrite each other.
3. Ordering
Do operations appear to execute in program order?
Not always.
The compiler and CPU may reorder instructions if no rule prevents them.
These three concepts — visibility, atomicity, and ordering — form the foundation of concurrent Java programming.
The Heart of the JMM: Happens-Before
If there is one concept every Java developer should understand, it is happens-before.
This rule defines visibility guarantees between actions.
If Action A happens-before Action B, then:
- Every write in A becomes visible to B
- B sees all side effects of A
- Ordering between A and B is guaranteed
This is the backbone of Java concurrency.
Without a happens-before relationship, thread behavior becomes unpredictable.
What Goes Wrong Without Synchronization?
Victor presented a common bug pattern using a flag.
class Flag {
boolean ready = false;
int value = 0;void writer() {
value = 42;
ready = true;
} void reader() {
while (!ready) {}
System.out.println(value);
}
}
You might expect the reader to print 42.
But without synchronization:
- The loop might never end
readymay never become visiblevaluemay print as0
Why?
Get Naijajug Info’s stories in your inbox
Join Medium for free to get updates from this writer.
Because writes can be reordered.
The CPU may publish:
ready = true;before:
value = 42;That creates stale reads and race conditions.
Tools Java Gives Us
Fortunately, Java provides synchronization mechanisms to establish order and visibility.
volatile
The volatile keyword guarantees:
- Visibility
- Ordering
Example:
volatile boolean ready;If one thread writes to a volatile variable, another thread reading it will always see the latest value.
However, volatile does not guarantee atomicity.
This means:
counter++;is still unsafe even if counter is volatile.
Use volatile for:
- Flags
- State indicators
- Shutdown signals
Not for compound operations.
synchronized
The synchronized keyword provides stronger guarantees.
Example:
synchronized(lock) {
sharedValue = 42;
}When a thread releases a lock, every write inside that block becomes visible to the next thread acquiring the same lock.
This creates a happens-before relationship.
That means synchronization guarantees:
- Visibility
- Ordering
- Mutual exclusion
This makes it one of Java’s most powerful concurrency tools.
final and Safe Publication
Immutable objects are naturally thread-safe — if constructed correctly.
Example:
class Point {
final int x;
final int y; Point(int x, int y) {
this.x = x;
this.y = y;
}
}Once construction completes, every thread sees the fully initialized object.
But there’s a caveat.
Never leak this during construction.
Bad example:
class Risky {
Risky() {
sharedRef = this;
init();
}
}Another thread may access the object before initialization finishes.
That breaks safety guarantees — even with final.
Common Mistakes Developers Make
Victor also highlighted several mistakes seen in real-world systems.
Double-Checked Locking Without volatile
Without volatile, threads may see partially constructed objects.
Assuming i++ Is Atomic
It isn’t.
Use:
AtomicInteger- synchronization
- locks
Mixing Locks
If writes use one lock and reads use none (or another lock), visibility is not guaranteed.
Publishing Objects Too Early
Avoid exposing references before construction completes.
Key Takeaways
Here’s the core message from the session:
Visibility is not automatic
Threads do not automatically see each other’s writes.
Instruction reordering is real
The JVM and CPU optimize aggressively.
Synchronization creates safety
Use volatile, synchronized, locks, or concurrent utilities.
Understand happens-before
This is the foundation of correct concurrent Java code.
Immutability reduces complexity
Favor immutable objects whenever possible.
As Victor emphasized, the hardest bugs in production are often not syntax errors or crashes — they are subtle concurrency issues that appear only under specific timing conditions.
Those are the bugs the Java Memory Model helps us understand and prevent.
Final Thoughts
The Java Memory Model can feel abstract at first, but it explains many mysterious multithreading bugs that developers encounter in production.
Understanding it changes how you think about concurrency.
You stop assuming:
“If I wrote it, another thread will see it.”
And start asking:
“What guarantees visibility here?”
That shift in thinking separates developers who use threads from developers who truly understand concurrency.
A big thanks to Victor Adepoju for breaking down such a complex topic into something practical and approachable during the NaijaJUG Lightning Talks session.






