ML
Concurrency

Thread Safety: Mutexes, Atomics, and Immutability

Four strategies for safe concurrent code, from heaviest to lightest — and the one most teams should default to.

June 05, 20258 min readConcurrencyPatterns

"Make this thread-safe" is a surprisingly empty instruction. There are at least four different tools, each with a different cost profile.

1. Mutex / synchronized

The default hammer. Wrap a critical section; only one thread at a time.

private final Object lock = new Object();
public void increment() {
  synchronized (lock) {
    counter++;
  }
}

Simple and safe. Cost: contention serialises threads, and bad lock discipline → deadlocks.

2. Atomics / lock-free primitives

Hardware compare-and-swap instructions expressed as AtomicLong, AtomicReference, sync/atomic in Go. For counters and single-field state, they're 5–20× faster than a mutex.

private final AtomicLong counter = new AtomicLong();
counter.incrementAndGet();

Cost: the API surface is tiny. Anything more than a single word (multi-field invariants, conditional updates) quickly turns into hand-rolled CAS loops that are easy to get wrong.

3. Immutability

If the value never changes, you cannot have a race on it. This is the secret weapon of functional-leaning codebases: instead of mutating a shared list, replace the reference with a new list. AtomicReference<List> becomes the entire concurrency story.

state.updateAndGet(old -> old.withAddedItem(x));

Cost: allocation pressure. Usually dwarfed by the win in simplicity.

4. Confinement / message passing

Give each piece of mutable state to exactly one thread and communicate via a queue/channel/actor. The goroutine + channel idiom; Erlang actors; the single-writer principle.

// Go: one goroutine owns the counter
for op := range ops {
  switch op.kind {
  case "inc": counter++
  case "get": op.reply <- counter
  }
}

Cost: higher latency per op (a channel hop). Reward: no locks in user code, easy to reason about.

The honest recommendation

Default to immutability. When a structure truly must be mutable, use confinement and expose only the channel/queue. Only reach for a mutex when both of the above are a bad fit. Atomics come out when a profiler tells you a mutex is the bottleneck — not before.

Smells to avoid

  • volatile used as a synchronisation primitive. It's a visibility primitive; it does not provide atomicity for compound operations.
  • Double-checked locking without volatile or @Volatile. Broken on modern memory models.
  • A static mutable field "just for now." It is never just for now.
SharePostLinkedIn

Reader Discussion

7 replies// weighed in

TopNewestAuthor
Add to the thread
Disagree, agree harder, or share your own experience…
Email instead →markdown okbe kind
  1. Hiếu Nguyễn· Full StackPushback

    tiny precision nit — volatile in Java provides visibility AND atomicity for single 32-bit reads/writes (long/double on legacy 32-bit JVMs is the exception). worth being precise because juniors read "visibility primitive" and reach for AtomicInteger when volatile is enough.

    Jun 12, 2025·1 week later·edited
  2. Maya Iyer· PlatformFrom experience

    Go's race detector is criminally under-used. Caught a bug in our scheduler we'd been running past for 6 months — turned out our "thread-safe" map was thread-safe in the way a chair is bulletproof. -race in CI, no exceptions.

    Jun 10, 2025·5 days later
  3. Tomáš Havel· Senior EngineerAgrees

    go channels solve a problem you don't have until you have it, and then they're the only thing that solves it. people reaching for sync.Mutex everywhere are usually one refactor away from a clean channel topology.

    Jun 11, 2025·6 days later
  4. Irene Chen· Staff EngineerAgrees

    "push the check into the write" is now the framing I use teaching juniors. once you see check-then-act anti-patterns you can't un-see them — they're hiding in literally every internal tool we have.

    Jun 07, 2025·2 days later
  5. Bảo Trần🇻🇳 Cần Thơ· Software EngineerStory

    Bọn em từng deadlock cổ điển 2-row trong ledger. Ordering by account_id ASC trước khi lock — 1 dòng commit, drop deadlock retries 98% trong tuần. Nhớ mãi vì PR đó merge lúc mình về quê ăn Tết.

    Jun 08, 2025·3 days later
  6. Isabella Costa· Junior EngineerKind words

    saved this. sharing at standup tomorrow — we've had exactly this problem for 2 sprints and nobody on the team had framed it this way 🙏

    Jun 07, 2025·2 days later
  7. Kenta Yamada· Tech LeadAsks

    would love a war-story follow-up. principles are clear; the actual debugging session is where the interesting stuff lives. there's a real shortage of "here's the dashboard, here's the thread we pulled, here's where we got stuck for 90 mins" content.

    Jun 09, 2025·4 days later

Worked on something similar? Email ducminhldm@gmail.com — I read every one. The good ones become future posts.

Comments seeded · live discussion via email