Browse Learn Clojure Foundations as a Java Developer

Evaluate Clojure Concurrency Overhead

Learn where Clojure concurrency primitives spend time, including atom retries, STM conflicts, agent queues, blocking work, and the coordination costs Java developers should measure before tuning.

Concurrency overhead is the extra work your program performs because multiple activities must coordinate. In Java, that cost often appears as lock contention, blocked threads, volatile reads, queue waits, or context switching. In Clojure, the same JVM realities still exist, but the hot spots usually move to atom retries, STM conflicts, agent queues, futures, blocking I/O, or repeated data conversion at Java boundaries.

The first performance skill is separating real coordination cost from unfamiliar Clojure syntax.

Where The Cost Comes From

Clojure tool Main overhead to watch
atom with swap! Compare-and-set retries, similar to contended AtomicReference or AtomicInteger updates.
ref with dosync Transaction retries when coordinated refs conflict, similar to optimistic retry logic.
agent Queue wait time and executor saturation, similar to work submitted to an ExecutorService.
future Thread-pool scheduling and blocking joins with @future, similar to Future.get.
Persistent collections Allocation for new values, offset by structural sharing rather than full copying.
Java interop Reflection, boxing, and conversion when adapter work sits in the hot path.

None of these costs make Clojure “slow” by default. They tell you what to measure.

Atom Retries Are A Contention Signal

swap! may call your update function more than once if another thread wins the compare-and-set first. That is correct behavior, but it means update functions must be pure and cheap.

This helper makes retry pressure visible for teaching. It is not a replacement for ordinary swap! in production code.

 1(defn counted-swap!
 2  [state f & args]
 3  (let [attempts (atom 0)]
 4    (loop []
 5      (swap! attempts inc)
 6      (let [old-value @state
 7            new-value (apply f old-value args)]
 8        (if (compare-and-set! state old-value new-value)
 9          {:value new-value
10           :attempts @attempts}
11          (recur))))))
12
13(def counters (atom {:orders 0}))
14
15(counted-swap! counters update :orders inc)
16;; => {:value {:orders 1}, :attempts 1}

If the same pattern regularly reports many attempts under load, the problem is not that atoms are broken. The problem is that too much work is fighting over one location.

STM Retries Are A Scope Signal

Software transactional memory (STM) is useful when multiple refs must change consistently. It is not a general-purpose speed switch.

 1(def inventory (ref {"sku-42" 10}))
 2(def reservations (ref []))
 3
 4(defn reserve!
 5  [sku customer-id]
 6  (dosync
 7    (let [available (get @inventory sku 0)]
 8      (when (pos? available)
 9        (alter inventory update sku dec)
10        (alter reservations conj {:sku sku
11                                  :customer-id customer-id})
12        true))))

The transaction should contain only the coordinated in-memory decision. Do not call a payment gateway, write a log file, or perform a slow database query inside dosync; retries can repeat the function body.

Decide What Kind Of Overhead You Have

Symptom Better next question
CPU is high and throughput stalls Is this retry contention, or is a pure computation hot path doing too much work?
Threads mostly wait Is the workload bound by the database, network, queue backpressure, or executor capacity?
GC pauses grow under load Which function allocates most, and are lazy sequences retaining more than expected?
Latency spikes on agent work Is the agent queue backing up, or is an action blocking the executor?
Java interop dominates profiles Can reflection, boxing, conversion, or per-item calls move outside the inner loop?

Reduce Shared State Before Tuning It

The fastest state update is often the one you do not coordinate globally.

 1(defn count-by-type
 2  [events]
 3  (reduce (fn [counts event]
 4            (update counts (:type event) (fnil inc 0)))
 5          {}
 6          events))
 7
 8(def totals (atom {}))
 9
10(defn record-batch!
11  [events]
12  (let [batch-counts (count-by-type events)]
13    (swap! totals merge-with + batch-counts)))

Each worker can summarize its own batch with ordinary immutable values, then perform one atom update. That is usually better than calling swap! once per event.

Review Checklist

Before changing primitives, ask:

Question Why it matters
Is the update function pure? Retries are safe only when the function has no side effects.
Is the state too centralized? A single atom can become a global bottleneck.
Is the transaction too wide? Wide STM transactions create more conflict opportunities.
Is blocking work inside coordination? Blocking holds scarce executor or transaction resources.
Are you measuring real workload shape? Microbenchmarks can hide queueing, I/O, and contention.

Knowledge Check

### Why must the function passed to `swap!` be free of side effects? - [x] It may run more than once when compare-and-set retries occur. - [ ] It always runs on a separate OS thread. - [ ] It is compiled only after the atom changes. - [ ] It disables JVM garbage collection during the update. > **Explanation:** `swap!` can retry if another thread changes the atom first. A pure update function can safely run again; an email send, log append, or database write cannot. ### What does frequent STM retry usually suggest? - [x] The transaction may be too wide or the refs are highly contended. - [ ] STM should be replaced by dynamic vars. - [ ] The refs are immutable and cannot be changed. - [ ] The JVM cannot run Clojure transactions concurrently. > **Explanation:** STM retries are expected under conflict. Frequent retries point to transaction scope, state shape, or workload contention. ### Which optimization usually reduces contention fastest? - [x] Summarize work locally and publish fewer shared updates. - [ ] Add more calls to `swap!` inside each loop. - [ ] Put blocking I/O inside `dosync`. - [ ] Replace all immutable maps with mutable Java maps. > **Explanation:** Local pure work avoids shared coordination. Publishing one summarized update per batch is often cheaper than coordinating every event.
Revised on Saturday, May 23, 2026