Thread Safety
A class is thread-safe if it behaves correctly when accessed from multiple threads simultaneously, regardless of the scheduling or interleaving of those threads, without additional coordination from the caller. Thread safety is a property of a class's implementation, not of its caller — a thread-safe class can be called from any number of threads in any order and will always satisfy its specification. Java provides a hierarchy of options for making code thread-safe: immutability (the strongest guarantee, zero synchronization needed), stateless objects (methods that use only local variables and parameters), thread confinement (state accessible to only one thread), volatile for single-variable visibility, synchronized blocks and methods, atomic classes for lock-free compound operations, and the concurrent data structures in java.util.concurrent. This entry covers the thread safety hierarchy in full, the specification of thread safety (what it means precisely), how to audit a class for thread safety, the difference between a thread-safe class and a thread-safe composition of thread-safe classes, common patterns (immutable objects, thread-local, synchronized delegation, copy-on-read), and how to document thread safety with annotations.
The Thread Safety Hierarchy
// ── Level 1: Immutable — zero synchronization needed ─────────────────
public final class ImmutablePoint {
private final int x;
private final int y;
public ImmutablePoint(int x, int y) {
this.x = x;
this.y = y;
}
public int x() { return x; }
public int y() { return y; }
public ImmutablePoint translate(int dx, int dy) {
return new ImmutablePoint(x + dx, y + dy); // returns NEW object — no mutation
}
}
// Safe to share across any number of threads — no synchronization needed:
ImmutablePoint origin = new ImmutablePoint(0, 0);
for (int i = 0; i < 100; i++) {
new Thread(() -> {
ImmutablePoint moved = origin.translate(1, 1); // origin never changes
System.out.println(moved.x() + "," + moved.y());
}).start();
}
// ── Level 2: Stateless — no fields, no synchronization needed ─────────
public class StatelessCalculator {
// No fields — all state in parameters and local variables:
public double hypotenuse(double a, double b) {
return Math.sqrt(a * a + b * b); // pure function — trivially thread-safe
}
public List<Integer> primes(int limit) {
List<Integer> result = new ArrayList<>(); // local — not shared
for (int n = 2; n <= limit; n++) {
boolean prime = true;
for (int i = 2; i * i <= n; i++) {
if (n % i == 0) { prime = false; break; }
}
if (prime) result.add(n);
}
return result; // returned to caller — not retained
}
}
// ── Level 3: Thread confinement via ThreadLocal ───────────────────────
public class DateFormatterPool {
// SimpleDateFormat is NOT thread-safe — give each thread its own:
private static final ThreadLocal<SimpleDateFormat> FORMATTER =
ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd"));
public static String format(Date date) {
return FORMATTER.get().format(date); // thread's own instance — no sharing
}
}
// ── Level 4: Volatile for single-variable visibility ──────────────────
public class ServiceState {
private volatile boolean active = false; // single boolean flag — volatile sufficient
public void activate() { active = true; } // atomic write
public void deactivate() { active = false; } // atomic write
public boolean isActive(){ return active; } // atomic read — always fresh value
}
// NOT sufficient for: active = !active (read-modify-write — needs synchronized or AtomicBoolean)
// ── Level 5: Atomic for lock-free compound operations ─────────────────
public class HitCounter {
private final AtomicLong hits = new AtomicLong(0);
private final AtomicLong misses = new AtomicLong(0);
public void recordHit() { hits.incrementAndGet(); }
public void recordMiss() { misses.incrementAndGet(); }
// CAS-based conditional update — atomic without synchronized:
public boolean compareAndSetHits(long expected, long update) {
return hits.compareAndSet(expected, update);
}
// Snapshot (not atomic between the two reads, but each read is atomic):
public long[] snapshot() { return new long[]{hits.get(), misses.get()}; }
}
// ── Level 6: Synchronized for compound multi-variable state ───────────
public class BankAccount {
private double balance;
private long txCount;
// BOTH fields updated together — must be synchronized as a unit:
public synchronized void deposit(double amount) {
balance += amount;
txCount++;
}
// Reading both atomically — synchronized ensures consistent view:
public synchronized double[] getBalanceAndTxCount() {
return new double[]{balance, txCount};
}
}Auditing a Class for Thread Safety
// ── Thread safety audit: a class with hidden state problems ──────────
public class AuditExample {
private int count = 0; // STATE 1: mutable, shared
private final List<String> items // STATE 2: mutable contents, shared
= new ArrayList<>();
private static int instanceCount = 0; // STATE 3: static, shared across all instances
// ── Audit: count ──────────────────────────────────────────────────
public synchronized void increment() { count++; } // protected ✓
public int getCount() { return count; } // NOT synchronized ✗ — data race on count
// ── Audit: items ──────────────────────────────────────────────────
public synchronized void addItem(String item) { items.add(item); } // protected ✓
public List<String> getItems() {
return items; // ✗ — returns mutable reference; caller can modify items without lock
}
public int itemCount() { return items.size(); } // ✗ — no lock; race with addItem
// ── Audit: instanceCount ─────────────────────────────────────────
public AuditExample() {
instanceCount++; // ✗ — static field, not synchronized
}
// ── Corrected version ─────────────────────────────────────────────
private volatile static int instanceCountFixed = 0; // fix: volatile for single increment
public synchronized int getCountFixed() { return count; } // fix: synchronized
public synchronized List<String> getItemsCopy() {
return new ArrayList<>(items); // fix: defensive copy — caller can't modify original
}
public synchronized int itemCountFixed() { return items.size(); } // fix: synchronized
public AuditExampleFixed() {
// Fix for static counter: use AtomicInteger:
}
private static final AtomicInteger safeInstanceCount = new AtomicInteger(0);
// In constructor: safeInstanceCount.incrementAndGet();
}
// ── Composition: thread-safe components ≠ thread-safe composition ──────
// ConcurrentHashMap is thread-safe, but compound operations on it are not:
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
map.put("counter", 0);
// RACE: two threads may both read 0, both compute 1, both put 1 — lost update:
Integer current = map.get("counter");
map.put("counter", current + 1); // not atomic even with ConcurrentHashMap
// FIX: use atomic compound method:
map.compute("counter", (k, v) -> v == null ? 1 : v + 1); // atomic
// ── Synchronized delegation: delegate to a thread-safe component ───────
public class ThreadSafeQueue<T> {
// Delegate all operations to a thread-safe blocking queue:
private final BlockingQueue<T> delegate = new LinkedBlockingQueue<>();
public boolean offer(T item) { return delegate.offer(item); }
public T poll() { return delegate.poll(); }
public int size() { return delegate.size(); }
public T waitFor() throws InterruptedException { return delegate.take(); }
// Compound: size + poll — NOT atomic (but usually acceptable for queues):
public Optional<T> pollIfNonEmpty() {
// Acceptable approximate check for queues where this is idiomatic:
return delegate.isEmpty() ? Optional.empty() : Optional.ofNullable(delegate.poll());
}
}
// ── @ThreadSafe / @NotThreadSafe documentation annotations ────────────
// From net.jcip:annotations or equivalent:
@ThreadSafe
public class SafeService {
private final AtomicLong requestCount = new AtomicLong(0);
public void handleRequest() { requestCount.incrementAndGet(); }
public long getRequestCount() { return requestCount.get(); }
}
@NotThreadSafe
public class UnsafeBuilder {
private final List<String> parts = new ArrayList<>(); // not thread-safe
public UnsafeBuilder add(String part) { parts.add(part); return this; }
public String build() { return String.join(", ", parts); }
// Intended usage: constructed and used by one thread, then result published
}Safe Publication, Escape, and Common Thread Safety Patterns
// ── Unsafe publication from constructor (this escape) ─────────────────
public class UnsafeEscape {
private final Map<String, String> data = new HashMap<>();
public static UnsafeEscape lastCreated; // another thread reads this
public UnsafeEscape(String key, String value) {
lastCreated = this; // ✗ — this escapes BEFORE constructor completes!
data.put(key, value); // another thread reading lastCreated sees data={} !
}
}
// ── Fix: factory method publishes only after construction ─────────────
public class SafeInit {
private final Map<String, String> data = new HashMap<>();
private SafeInit(String key, String value) {
data.put(key, value); // constructor completes fully
}
// volatile: safe publication — write happens-before any subsequent read:
public static volatile SafeInit lastCreated;
public static SafeInit create(String key, String value) {
SafeInit instance = new SafeInit(key, value); // fully constructed
lastCreated = instance; // volatile write — published safely
return instance;
}
}
// ── Initialization-on-demand holder: thread-safe lazy singleton ────────
public class Singleton {
private Singleton() {}
private static class Holder {
// Initialized exactly once, by the class loader:
static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return Holder.INSTANCE; // class loading is thread-safe — no synchronized needed
}
}
// ── Copy-on-read: safe publication of collection contents ─────────────
public class SafeRegistry {
private final List<String> entries = new ArrayList<>();
private final Object lock = new Object();
public void register(String entry) {
synchronized (lock) { entries.add(entry); }
}
// Returns a snapshot — caller has their own copy, modifications don't matter:
public List<String> getAll() {
synchronized (lock) { return new ArrayList<>(entries); }
}
}
// ── CopyOnWriteArrayList: automatic copy-on-write ─────────────────────
CopyOnWriteArrayList<String> cowList = new CopyOnWriteArrayList<>();
cowList.add("a");
cowList.add("b");
// Iteration: snapshot taken at iterator creation — no locking needed:
for (String s : cowList) {
cowList.add("c"); // modification: creates new array — iterator sees old version
}
System.out.println(cowList); // [a, b, c] (add succeeded)
// ── Final fields and safe publication guarantee ────────────────────────
public class SafeState {
public final int x;
public final int y;
public final List<String> names; // final reference — list contents still mutable!
public SafeState(int x, int y, List<String> names) {
this.x = x;
this.y = y;
this.names = Collections.unmodifiableList(new ArrayList<>(names)); // defensive copy + unmodifiable
}
// x, y, and the contents of names are safely published — final guarantee + defensive copy
}
// ── Documenting thread safety with annotations ─────────────────────────
@ThreadSafe
public class ThreadSafeCache<K, V> {
// ConcurrentHashMap is @ThreadSafe:
@GuardedBy("itself") // ConcurrentHashMap guards its own state
private final ConcurrentHashMap<K, V> cache = new ConcurrentHashMap<>();
public V getOrCompute(K key, Function<K, V> loader) {
// computeIfAbsent is atomic in ConcurrentHashMap:
return cache.computeIfAbsent(key, loader);
}
public void invalidate(K key) { cache.remove(key); }
public int size() { return cache.size(); }
}
@NotThreadSafe // document that callers must synchronize externally
public class UnsafeCache<K, V> {
private final HashMap<K, V> cache = new HashMap<>();
public V getOrCompute(K key, Function<K, V> loader) {
return cache.computeIfAbsent(key, loader);
}
}