Browse Learn Clojure Foundations as a Java Developer

Replace Java Locking Habits with Clojure State Boundaries

Use Java locking knowledge to recognize where Clojure needs an atom, ref transaction, agent, queue, or plain immutable value instead of synchronized blocks and manual unlock discipline.

Java developers learn to ask, “Which lock protects this data?” Clojure pushes a different first question: “Why is this data shared and mutable at all?” That shift matters more than the syntax difference between synchronized and swap!.

Locking: Coordinating access to mutable state so that one thread at a time can observe or change a protected invariant.

In Clojure, you still run on the JVM and can still use Java locks when a Java API requires them. But idiomatic Clojure usually reduces the amount of code that needs mutual exclusion by moving ordinary work into immutable values and isolating state changes behind reference types.

Translate The Locking Intent

Do not translate Java mechanisms one-for-one. Translate the reason the mechanism was used.

Java mechanism What it usually protects Clojure replacement to consider
synchronized method One object’s mutable fields Atom holding an immutable map when the whole invariant fits in one value
ReentrantLock Explicit critical section Atom, ref transaction, or a Java lock only at an interop boundary
Semaphore Limited permits for a scarce resource Java Semaphore, bounded executor, or queue when capacity control is the real requirement
ReadWriteLock Many readers, few writers Immutable snapshots in an atom so readers do not lock
Multiple locks Cross-object invariant Refs inside one dosync transaction

Java locking knowledge remains valuable because it helps you recognize contention, deadlock risk, visibility bugs, and accidental shared state. The Clojure move is to make those risks smaller and more visible.

A Java Counter With A Monitor

This Java class uses a monitor to protect one mutable field:

 1public final class Counter {
 2    private int count = 0;
 3
 4    public synchronized void increment() {
 5        count++;
 6    }
 7
 8    public synchronized int value() {
 9        return count;
10    }
11}

The important detail is not just the synchronized keyword. The important detail is the invariant: every read and write of count must go through the same monitor. If another method touches count without synchronization, the class is no longer correct.

The Clojure Shape

In Clojure, the mutable cell is explicit and small. The value inside it is immutable.

1(def counter (atom {:count 0}))
2
3(defn increment! []
4  (swap! counter update :count inc))
5
6(defn value []
7  (:count @counter))

swap! applies a function to the current value and installs the result atomically. The function may be retried, so it must be pure: no logging, I/O, random UUID creation, or database writes inside the function passed to swap!.

The state boundary is now easier to review:

Question Java synchronized class Clojure atom
Where is mutation? Hidden inside fields Visible at the atom
What protects updates? A monitor chosen by convention Atomic compare-and-set through swap!
Can readers keep a stable snapshot? Only if they copy or keep holding a lock Yes, dereference returns an immutable value
Main risk Unsynchronized field access or lock-order bugs Side effects or expensive work inside swap!

When A Lock Is Still The Right Tool

Clojure does not remove every reason for Java locking. Use a Java lock or concurrency utility when the problem is genuinely about a Java resource, not Clojure application state.

Good reasons include:

  • A Java library documents that a mutable object must be accessed under a specific lock.
  • You need tryLock, interruptible waiting, fairness, or permit accounting with exact operational behavior.
  • You are wrapping a legacy mutable cache while migrating the rest of the code to immutable values.
  • You are protecting a non-Clojure resource such as a file handle, socket session, native handle, or pooled connection object.

Even then, keep the lock at the boundary. Convert into immutable Clojure data as soon as practical.

 1(import '[java.util.concurrent.locks ReentrantLock])
 2
 3(def lock (ReentrantLock.))
 4(def legacy-cache (java.util.HashMap.))
 5
 6(defn read-legacy-cache [k]
 7  (.lock lock)
 8  (try
 9    (.get legacy-cache k)
10    (finally
11      (.unlock lock))))

This wrapper is intentionally small. The rest of the application should not know about legacy-cache, HashMap, or the lock.

Common Migration Mistakes

The most common mistake is to write Java-style classes in Clojure and then protect them with ad hoc locks. That keeps the hardest part of Java concurrency while giving up Java’s class-level encapsulation.

Mistake Why it hurts Prefer
Store mutable Java objects in many vars No single owner for mutation One atom/ref/agent that owns the boundary
Put side effects inside swap! Retry can repeat the effect Compute new state in swap!, perform effects after
Use locks for every shared read Readers become serialized Dereference immutable snapshots
Replace every lock with one atom Cross-state invariants can tear Use refs and dosync when values must change together
Ignore Java lock contracts in interop Clojure cannot fix unsafe Java usage Honor the Java API at the boundary

Choosing Quickly

Use this quick decision flow during review:

  1. If the value never needs to change, pass immutable data.
  2. If one independent value changes synchronously, use an atom.
  3. If several values must change together, use refs with dosync.
  4. If independent work should happen later and in order, use an agent.
  5. If the problem is resource capacity or Java API safety, use the appropriate JVM concurrency utility at the edge.

The practical win is not “no locks anywhere.” The win is that a reader can see where state is, which rule protects it, and which parts of the program are just pure value transformations.

Knowledge Check

### A Java class uses `synchronized` only to protect one string-to-integer counter table. What is usually the first Clojure design to consider? - [x] An atom containing an immutable map - [ ] A global mutable `java.util.HashMap` - [ ] A separate `ReentrantLock` for each key - [ ] A ref for every integer value > **Explanation:** If the table is one independent state value, an atom holding an immutable map is usually the simplest Clojure shape. Refs are useful when multiple independent references must change together. ### Why must the function passed to `swap!` avoid side effects? - [x] Clojure may retry the function if another thread updates the atom first - [ ] `swap!` runs only on a background thread - [ ] Atoms cannot contain maps - [ ] Side effects are impossible on the JVM > **Explanation:** `swap!` uses retry logic. A side effect inside the update function can happen more than once even though only one final state is installed. ### When might a Java `ReentrantLock` still be appropriate in Clojure code? - [x] When wrapping a legacy mutable Java object with a documented locking requirement - [ ] Whenever two Clojure functions call each other - [ ] Whenever a persistent vector is read by many threads - [ ] Whenever an atom is dereferenced > **Explanation:** Clojure's immutable values and reference types reduce routine locking, but Java interop boundaries may still require Java locking semantics.
Revised on Saturday, May 23, 2026