Compare Java threads, locks, volatile fields, atomics, concurrent collections, and executor services with the Clojure state and coordination tools you should reach for first.
Java’s concurrency toolbox is broad because Java must support many programming styles: mutable objects, synchronized methods, low-level locks, atomics, concurrent collections, futures, executors, and reactive libraries. Clojure runs on that same JVM, but idiomatic Clojure starts from a different baseline: immutable values first, explicit changing references second.
This page is a translation guide. It does not say “never use Java concurrency.” It helps you decide when a familiar Java mechanism maps to a simpler Clojure shape.
| Java mechanism | What it gives you | Clojure translation |
|---|---|---|
Thread |
A unit of JVM execution. | Usually hidden behind futures, agents, core.async, web servers, or Java libraries. |
synchronized |
Mutual exclusion and monitor-based visibility. | Often replaced by atoms, refs/STM, immutable values, or one explicit lock at an interop boundary. |
ReentrantLock |
Explicit lock/unlock control. | Keep for Java interop or specialized cases; avoid spreading it through Clojure domain logic. |
volatile |
Visibility for a field reference. | Atoms and Vars provide clearer managed references for most Clojure code. |
AtomicReference / AtomicInteger |
Lock-free atomic updates. | Atom is the closest everyday Clojure equivalent. |
ConcurrentHashMap |
Concurrent mutable map operations. | Use when a Java library or performance profile requires it; otherwise consider immutable maps inside an atom. |
ExecutorService |
Thread-pool-backed task execution. | Used underneath many abstractions; choose futures, agents, channels, or library APIs at the Clojure level. |
Java knowledge still matters. You will debug thread dumps, blocked I/O, executor saturation, and library thread-safety. The difference is that your Clojure application code should not recreate every Java synchronization pattern by habit.
In Java, the common move is to protect a critical section:
1public synchronized void addItem(LineItem item) {
2 items.add(item);
3 total = total.add(item.price());
4}
The method looks small, but correctness depends on every other method respecting the same lock. Clojure usually moves the invariant into a value transition:
1(defn add-item [order item]
2 (-> order
3 (update :items conj item)
4 (update :total + (:price item))))
5
6(def order-state
7 (atom {:items [] :total 0M}))
8
9(defn add-item! [item]
10 (swap! order-state add-item item))
The pure function add-item is reviewable and testable without threads. The atom marks the single place where the order identity changes.
| Situation | Why Java may still be right |
|---|---|
| A Java library requires a specific executor or lock type. | Interop contracts matter more than stylistic purity. |
A hot cache is proven to need ConcurrentHashMap. |
Measurement can justify a mutable concurrent structure. |
| You are implementing a low-level adapter. | The boundary may need Java primitives while the core stays value-oriented. |
| Thread dumps show blocked monitors or executor starvation. | JVM operational debugging still uses Java concepts. |
| Existing Java code owns the state. | Wrap it carefully instead of pretending it is immutable. |
The design goal is containment. If you need a Java concurrency primitive, put it behind a small namespace or adapter and expose a simpler Clojure API to the rest of the program.
| If your Java design would use… | First Clojure option to consider |
|---|---|
AtomicInteger counter |
Atom with swap! |
| Synchronized setter on one object | Atom holding one immutable value |
| Lock around several related fields | One immutable aggregate, or refs in dosync |
| Executor task returning a value | future, promise, or library async API |
| Single-threaded worker object | Agent or channel consumer |
| Concurrent producer/consumer queue | core.async channel or Java queue at the boundary |
| Thread-local request context | Explicit arguments; dynamic Var only when justified |
Do not translate mechanically. A Java ConcurrentHashMap may become an atom holding a map, a database table, a cache library, or no shared state at all after the design is simplified.