Browse Learn Clojure Foundations as a Java Developer

Carry Java Memory Model Lessons into Clojure

Apply Java Memory Model instincts to Clojure by separating immutable values, safe publication, atom visibility, volatile interop, and the few places where JVM memory rules still dominate correctness.

Clojure does not exempt you from the Java Memory Model (JMM). It runs on the JVM, calls Java code, and uses JVM synchronization mechanisms under its reference types. What Clojure changes is the amount of mutable shared memory you expose directly.

Java Memory Model: The JVM-level set of rules that defines when writes by one thread become visible to another thread and which reorderings are allowed.

For Java engineers, the right mindset is not “forget volatile and synchronization.” It is “use the JMM knowledge to keep mutable publication points explicit.”

What Still Matters

Java memory visibility issues usually appear when a thread reads shared mutable state that another thread wrote without a happens-before relationship. Clojure’s immutable values make many reads simpler, but stateful references and Java interop still need clear publication semantics.

Concern Java symptom Clojure shape Practical rule
Visibility Thread sees stale field value Dereference atom/ref/agent, promise, future, or delay Share through a synchronization-aware abstraction
Atomicity counter++ loses updates swap!, alter, send Update through the owning reference
Ordering Writes appear out of expected order Explicit handoff through queues, futures, promises, refs, or agents Do not rely on timing or sleep
Safe publication Object escapes before fully constructed Immutable value returned or installed behind a reference Construct first, publish once
Java interop Mutable Java object read unsafely Boundary wrapper Follow the Java API’s thread-safety contract

The JMM is still the floor. Clojure’s design gives you better defaults above that floor.

The Java Visibility Bug

This Java example has no reliable visibility guarantee for running:

 1public final class Worker {
 2    private boolean running = true;
 3
 4    public void stop() {
 5        running = false;
 6    }
 7
 8    public void runLoop() {
 9        while (running) {
10            doWork();
11        }
12    }
13}

Java developers typically fix that by using volatile, synchronization, or a higher-level concurrency primitive:

 1public final class Worker {
 2    private volatile boolean running = true;
 3
 4    public void stop() {
 5        running = false;
 6    }
 7
 8    public void runLoop() {
 9        while (running) {
10            doWork();
11        }
12    }
13}

That fixes visibility for the flag, but it does not make compound updates atomic. volatile is not a substitute for protecting an invariant.

The Clojure Version

In Clojure, an atom can make the state boundary and visibility point explicit:

1(def running? (atom true))
2
3(defn stop! []
4  (reset! running? false))
5
6(defn run-loop! []
7  (while @running?
8    (do-work!)))

The dereference of the atom reads through Clojure’s synchronization-aware reference. The update happens through reset!. The shared flag is not a plain mutable field hidden somewhere in an object graph.

For a counter, the important distinction is atomicity:

1(def counter (atom 0))
2
3(defn increment! []
4  (swap! counter inc))

The Java mistake would be to read, compute, and write through separate operations. In Clojure, swap! owns the read-modify-write loop.

Safe Publication With Values

The simplest Clojure publication strategy is to build immutable data completely before another thread can see it.

1(defn build-config [env]
2  {:endpoint (get env "SERVICE_URL")
3   :timeout-ms 2500
4   :retries 3})
5
6(def config (delay (build-config (System/getenv))))

Consumers dereference config:

1(defn endpoint []
2  (:endpoint @config))

The reader gets a fully constructed immutable map. No consumer needs to coordinate access to individual fields.

Where Java Rules Reappear

The JMM becomes visible again when Clojure code holds mutable Java objects, calls Java callbacks, or crosses executor boundaries. Treat those places as concurrency edges.

Boundary Risk Safer pattern
Mutable Java bean stored in an atom Atom protects the reference, not internal field mutation Store immutable Clojure data or replace the bean as a whole
Java collection mutated after publication Readers may observe changing internals Convert with into before sharing
Callback invoked on Java thread pool Dynamic vars and request context may not follow automatically Pass explicit immutable context
Thread/sleep used for coordination Timing is not a happens-before rule Use promise, future, latch, queue, or executor API
Lazy sequence crosses threads Work may run later on a different thread than expected Realize at the boundary when timing matters

An atom holding a mutable Java object is a common trap:

1(def users (atom (java.util.ArrayList.)))
2
3(defn add-user! [user]
4  (.add @users user))

The atom protects only replacement of the ArrayList reference. It does not make .add safe. Prefer storing an immutable vector and updating it through swap!:

1(def users (atom []))
2
3(defn add-user! [user]
4  (swap! users conj user))

Review Checklist

When reviewing Clojure concurrency code with Java memory-model instincts, ask:

Review question Why it matters
Is shared state behind an atom, ref, agent, promise, future, channel, queue, or documented Java abstraction? Plain shared mutation needs a visibility story.
Does the update need atomic read-modify-write semantics? Use swap!, alter, or a Java atomic/concurrent class rather than separate read and write steps.
Are mutable Java objects converted before publication? Clojure references do not protect internal mutation of Java objects.
Are lazy values crossing thread boundaries intentionally? Realization timing can move work and side effects.
Is coordination expressed with a real handoff primitive? Sleeping and polling are not reliable synchronization.

The strongest Clojure code makes the happens-before story boring: immutable data is safe to share, and mutable publication points are small enough to audit.

Knowledge Check

### What does the Java Memory Model still control for Clojure programs? - [x] Visibility and ordering behavior for code running on the JVM - [ ] Only Java source files compiled with `javac` - [ ] Only garbage collection behavior - [ ] Only Clojure macros > **Explanation:** Clojure runs on the JVM, so JVM memory visibility and ordering rules still matter, especially at mutable state and Java interop boundaries. ### Why is an atom containing a mutable `ArrayList` not enough to make `.add` safe? - [x] The atom protects replacement of the reference, not unsynchronized internal mutation of the list - [ ] Atoms cannot hold Java objects - [ ] `ArrayList` becomes immutable inside an atom - [ ] Dereferencing an atom copies the Java object > **Explanation:** The atom coordinates changes made through atom operations. Directly mutating the object inside bypasses that coordination. ### Which coordination approach is more reliable than using `Thread/sleep` to wait for another thread? - [x] A real handoff primitive such as a promise, future, latch, queue, or executor result - [ ] A longer sleep interval - [ ] Repeating the read until it looks correct - [ ] A comment explaining the expected timing > **Explanation:** Sleeping is a timing guess, not a synchronization rule. A handoff primitive gives the program an explicit visibility and completion relationship.
Revised on Saturday, May 23, 2026