🧵 Concurrency Fundamentals · Advanced

Thread safety strategies in Java

Immutability, confinement, synchronization, thread-safe classes.

🧩 The mysteryLocks, atomics, concurrent maps... Before you reach for any of them, ask one question: does this data even need to be shared, or changed?

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.

🔮 Predict it

Your turn

What does this print?

Map<String, Integer> m = Map.of("a", 1);
m.put("b", 2);
System.out.println(m);
  1. Throws UnsupportedOperationException
  2. {a=1, b=2}
  3. 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.

⚠️ The trap

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

✗ Shared mutable
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.

✓ Shared immutable
static final DateTimeFormatter FMT =
    DateTimeFormatter.ofPattern("yyyy-MM-dd");

Immutable and thread-safe: share it with every thread.

💼 In the real world

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

  1. Confinement: data used by only one thread needs no locks
  2. Immutability: objects that never change are always thread-safe
  3. Synchronization: guard every access with the same lock
  4. Delegation: lean on java.util.concurrent classes
🤯 Did you know?

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);
  1. Throws UnsupportedOperationException
  2. [a, b, c]
  3. [a, b]
  4. 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?

  1. Use the immutable, thread-safe DateTimeFormatter instead
  2. Mark the static field volatile
  3. Make the field final
  4. 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.

Next: give each thread its own private copy of a value with ThreadLocal, and see how it can leak one user's data into another's request.