Sitemap
Naijajug Info

Naijajug Info

Understanding the Java Memory Model: The Invisible Rules Behind Concurrent Java Programs

5 min readJul 1, 2026

--

Press enter or click to view image in full size

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:

  1. Read value
  2. Increment
  3. 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
  • ready may never become visible
  • value may print as 0

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.

Join the Naija Java User Group

Want more Java-focused technical sessions, community discussions, and hands-on learning?

Follow the Naija Java User Group (NaijaJUG) community for future meetups, workshops, and lightning talks.

NaijaJUG Website
NaijaJUG Community

--

--