Thread Safety Levels
Code is not thread-safe or unsafe in one bucket. There are levels: immutable, thread-confined, conditionally thread-safe, fully thread-safe, and lock-free. Knowing the level a class promises is the difference between safe code and a postmortem.
Diagram
What it is
"Thread-safe" is not one thing. It's a ladder of five different guarantees, each one stronger and more expensive than the last (see the diagram above). When two engineers argue about whether some object is "safe to use from multiple threads," they're usually arguing about which rung applies.
A short characterisation of each rung, top of ladder to bottom:
- Immutable. No writes after construction. Nothing to race on. Cheapest because no synchronisation runs at all.
- Thread-confined. Mutates, but only one thread ever touches it. Safe by isolation. Hazard: confinement leaks if the object is accidentally stashed in a global.
- Conditionally thread-safe. Each method call is safe; sequences of calls might not be. Trap:
if (!map.containsKey(k)) map.put(k, v)looks like one statement but is two operations and races. - Fully thread-safe. Every method and every reasonable composition is safe. What most people mean when they say "thread-safe."
- Lock-free. Thread-safe plus non-blocking: no thread can be stalled forever by another that's holding a lock and got preempted. Faster under contention, harder to write.
Why the level matters
When two engineers argue about whether some shared object is "safe to use from multiple threads," they are usually arguing about which level applies. Bringing the levels into the conversation usually ends it: "It is conditionally thread-safe, so this whole sequence needs to be made atomic at the call site," is a precise sentence.
Document the level A library class should state in one sentence which level it is. A reader of someone else's class should find that sentence (or write it in a comment). Most production threading bugs come from a caller assuming a higher level than the library actually provides.
When to step up the level
Start at immutable. If that's not possible, go thread-confined. If that's not possible, design for fully thread-safe with the standard concurrent collections. Only reach for lock-free after measuring contention as the bottleneck and finding the simpler levels do not give the required throughput.
The mistake is going lock-free first because it sounds advanced. Lock-free code is genuinely harder to write, harder to verify, and easy to get subtly wrong. Use it when needed, not when it sounds impressive.
Implementations
Once constructed, the object cannot change. Any number of threads can read it. The final modifiers and lack of setters make this guarantee structural, not just convention. String, Integer, and Java's Instant are immutable.
1 public final class Money {
2 private final long cents;
3 private final String currency;
4
5 public Money(long cents, String currency) {
6 this.cents = cents;
7 this.currency = currency;
8 }
9
10 public Money plus(Money other) { // returns a NEW object
11 if (!currency.equals(other.currency)) throw new IllegalArgumentException();
12 return new Money(cents + other.cents, currency);
13 }
14
15 public long cents() { return cents; }
16 public String currency() { return currency; }
17 }ConcurrentHashMap.get and put are individually thread-safe. The compound operation "check then put" is not. Two threads can both see no entry and both insert. Use putIfAbsent or computeIfAbsent for the compound operation.
1 ConcurrentHashMap<String, Counter> counters = new ConcurrentHashMap<>();
2
3 // BAD: check-then-act is racy even though each call is safe
4 if (!counters.containsKey(key)) {
5 counters.put(key, new Counter()); // two threads may both reach here
6 }
7 counters.get(key).inc();
8
9 // GOOD: one atomic operation
10 counters.computeIfAbsent(key, k -> new Counter()).inc();Key points
- •Immutable: no writes after construction. Always safe to share.
- •Thread-confined: only ever touched by one thread. Safe by isolation, not by locking.
- •Conditionally thread-safe: individual methods are safe, but compound operations need external synchronisation.
- •Fully thread-safe: every method and every reasonable sequence of calls is safe under concurrent use.
- •Lock-free: thread-safe AND no thread can be permanently blocked by another. Stronger than fully thread-safe.