Thread safety strategies in Java
Immutability, confinement, synchronization, thread-safe classes.
Four strategies
Every race needs shared, mutable, unguarded state. Remove one ingredient: Confinement: don't share it. Immutability: don't change it. Synchronization: guard every access with the same lock. Delegation: use thread-safe classes like ConcurrentHashMap or AtomicLong. Prefer the earlier ones: simpler, and they can't deadlock.
Confinement
Data used by only one thread needs no locks. Local variables and objects created inside a method call, like a local ArrayList, are confined to the thread running that call. Many web handlers are thread-safe simply because each request works on its own objects.
List<Item> handle(Request r) {
var result = new ArrayList<Item>();
for (var id : r.ids())
result.add(load(id));
return result; // never shared
}Immutability
An object that never changes after construction is always thread-safe: there's nothing to race on. String, List.of(...), DateTimeFormatter and records with immutable components can be shared freely, with no locks.
Your turn
What does this print?
Map<String, Integer> m = Map.of("a", 1);
m.put("b", 2);
System.out.println(m);Throws UnsupportedOperationException{a=1, b=2}Compile error
Show the answer
Map.of (like List.of and Set.of) returns an unmodifiable collection: any attempt to change it throws UnsupportedOperationException. Because nobody can modify it, it's safe to share between threads.
Records aren't deeply immutable
A record's fields are final, but a final reference to an ArrayList still lets anyone add to the list. The record is only as immutable as its components. Fix: copy into an unmodifiable list in the compact constructor.
record Team(String name, List<String> members) {
Team {
members = List.copyOf(members);
}
}A shared date formatter
static final SimpleDateFormat FMT =
new SimpleDateFormat("yyyy-MM-dd");Keeps mutable state while formatting, so concurrent calls garble each other's dates. final or volatile don't help.
static final DateTimeFormatter FMT =
DateTimeFormatter.ofPattern("yyyy-MM-dd");Immutable and thread-safe: share it with every thread.
The stateful singleton
In Spring, a @Service is one shared object used by every request thread at once. Put a mutable field in it, or share a plain ArrayList or HashMap, and concurrent writes corrupt it. Keep services stateless (confinement), config immutable, and shared counters in atomics or concurrent collections (delegation).
Key takeaways
- Confinement: data used by only one thread needs no locks
- Immutability: objects that never change are always thread-safe
- Synchronization: guard every access with the same lock
- Delegation: lean on java.util.concurrent classes
java.util.Date is mutable, one big reason Java 8 added the immutable java.time API (JSR-310), led by Joda-Time's author Stephen Colebourne.
Practice questions
What does this print?
List<String> names = List.of("a", "b");
names.add("c");
System.out.println(names);- Throws UnsupportedOperationException
- [a, b, c]
- [a, b]
- Compile error
Check your answer
Throws UnsupportedOperationException. List.of returns an unmodifiable list. Any attempt to change it throws, which is exactly why such lists can be shared freely between threads.
A SimpleDateFormat in a static field is shared by all request threads, and dates occasionally come out garbled. Best fix?
- Use the immutable, thread-safe DateTimeFormatter instead
- Mark the static field volatile
- Make the field final
- Create the formatter in a static initializer
Check your answer
Use the immutable, thread-safe DateTimeFormatter instead. SimpleDateFormat keeps mutable internal state while formatting, so concurrent calls corrupt each other. DateTimeFormatter is immutable and safe to share.