9. Concurrent Collections, Shared State & Debugging Tools

Thread-safe collections out of the box, immutability as a concurrency strategy, and how each language actually helps you find a data race or a stuck process in production.

This closing section is deliberately practical: given everything else in this manual, how do you actually catch a bug, or find one that's already loose in production? Go's built-in race detector is the standout tool of the five -- nothing else here comes close to catching data races automatically. The rest is a mix of rich concurrent-collection libraries (Java), thread/coroutine dump tooling (jstack, py-spy), and the simple fact that a single-threaded runtime (JS, and Python's GIL for many cases) structurally prevents an entire category of bug from occurring at all.

See roadmap: Race Conditions & Critical Sections

Language Verdict: Pros, Cons & Recommendation

Go

5/5
  • -race is a genuine, mainstream, everyday-usable automated data-race detector -- the standout tool of this whole manual
  • SIGQUIT goroutine dumps plus automatic all-blocked deadlock detection are built in with zero setup
  • sync.Map's narrow, unusual fit means it's frequently reached for incorrectly instead of a mutex-guarded map
  • No language-level immutability enforcement -- 'immutable' structs are convention only, unlike Java's record or Python's frozen dataclass

Java

4/5
  • Richest built-in concurrent-collections family (ConcurrentHashMap, CopyOnWriteArrayList, BlockingQueue)
  • jstack's explicit 'Found one Java-level deadlock' detection is genuinely useful, mature tooling
  • No automated data-race detector at all, unlike Go
  • record/List.of() immutability helps, but there's no single 'always safe to share' guarantee as strong as a frozen dataclass or Object.freeze

Kotlin

4/5
  • Full access to Java's concurrent collections, plus StateFlow as a coroutine-native immutable-snapshot tool
  • kotlinx-lincheck gives genuinely rigorous testing for hand-rolled concurrent algorithms
  • Plain jstack dumps are often not useful for diagnosing a stuck coroutine -- need the separate DebugProbes tool
  • Same lack of an automated whole-program race detector as Java

Python

3/5
  • frozen=True dataclasses give real, enforced immutability with zero extra ceremony
  • py-spy gives a genuinely easy, no-instrumentation-needed production stack dump
  • No dedicated fully-safe-for-compound-access concurrent collection beyond queue.Queue
  • No automated race detector at all -- and the GIL's partial safety net makes people less likely to look for one

JavaScript

4/5
  • Object.freeze gives real enforced immutability with no library needed
  • The entire 'concurrent collection' and 'data race' problem space is structurally absent for the common single-threaded case
  • No good single-dump diagnostic for a stuck async chain -- 'stuck' looks like pending work, not a blocked thread
  • Zero tooling at all for the rare but real SharedArrayBuffer-race case
Recommendation: If Go is on the table for a CPU/concurrency-heavy service, mention -race explicitly -- it's the single most differentiated piece of tooling in this whole manual. Otherwise, default to each language's native concurrent-collection type over a plain one guarded by hand, and know your production stack-dump tool (jstack, goroutine SIGQUIT dump, py-spy, Node's diagnostic report) before you need it under pressure.

Concurrency Mechanics, Side by Side

Thread-Safe Collections Out of the Box

Must-know
var mu sync.Mutex m := make(map[string]int) // plain map -- NOT safe for concurrent use, guard with mu var sm sync.Map // built-in concurrent map, but a narrower, different-shaped API sm.Store("key", 1) value, ok := sm.Load("key")

Go's built-in map type is explicitly NOT safe for concurrent read+write access (the runtime will detect this and crash with 'fatal error: concurrent map read and map write' in many cases, which is at least better than silent corruption) -- the idiomatic fix for most code is simply a mutex-guarded plain map. sync.Map exists for a narrower set of access patterns (many goroutines reading/writing disjoint keys, few iterations) and is explicitly documented as usually the wrong choice compared to a mutex-guarded map unless you specifically match that pattern.

Immutable/Persistent Data Structures as a Concurrency Strategy

Recommended
type Config struct { Timeout int; Retries int } // no built-in immutability enforcement var configPtr atomic.Pointer[Config] configPtr.Store(&newConfig) // readers atomic.Pointer.Load() a whole new struct

Go has no language-level immutability enforcement (no final/val equivalent for struct fields) -- 'immutable by convention' means simply never mutating a struct after constructing it and relying on team discipline, not a compiler guarantee. atomic.Pointer[T] (generics, 1.19+) gives the same swap-a-whole-snapshot pattern as Java's AtomicReference.

Detecting Data Races Automatically

Must-know

Go's race detector is the standout tool in this entire manual -- nothing else here comes close to automatically catching a data race before it causes a production incident.

$ go test -race ./... $ go run -race main.go // Output on a real race: // WARNING: DATA RACE // Write at 0x00c0000a4010 by goroutine 7: ... // Previous read at 0x00c0000a4010 by goroutine 6: ...

This is a genuine, oft-cited Go differentiator: -race instruments every memory access and goroutine synchronization event at compile time (using Google's ThreadSanitizer under the hood) and reports the exact two conflicting accesses, with both goroutines' stack traces, the moment a real race actually occurs during a test run. The trade-offs are real too -- 5-10x memory overhead and 2-20x slower execution -- so it's a testing/CI tool, not something you run in production, and it only catches races that actually execute during the run, not every theoretically possible one.

Diagnosing a Stuck/Deadlocked Process in Production

Must-know
$ kill -QUIT <pid> # or SIGABRT -- prints a full goroutine dump to stderr // Every goroutine's state and stack, automatically, including the // deadlock-detection crash covered earlier if ALL goroutines are blocked. import "net/http/pprof" // also exposes /debug/pprof/goroutine over HTTP

Sending SIGQUIT dumps every goroutine's stack automatically, no extra tooling required, and the runtime's own all-goroutines-blocked deadlock detection (covered in the locks section) means the worst case often crashes with full diagnostics rather than silently hanging forever. net/http/pprof is the additional, widely-used tool for live production inspection (goroutine counts, blocking profiles) without even needing to signal the process.

Further Reading