☕ Java

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

Thread safety is not binary — there is a spectrum of techniques with different strength, performance, and complexity tradeoffs. Understanding the full hierarchy allows choosing the right technique for each situation rather than defaulting to coarse synchronization everywhere. Immutability is the strongest guarantee. An immutable object's state cannot change after construction. Because state never changes, there is nothing to synchronize — multiple threads can read the object's fields simultaneously with no risk of seeing inconsistent state. Immutability requires all fields to be final, all mutable object references held in fields to never be exposed or mutated, and the constructor to complete before the object reference is published to other threads (the JMM guarantees that final fields are visible after construction completes, without any additional synchronization). Java's String, Integer, BigDecimal, and all other boxed primitives are immutable. Stateless objects — objects (or methods) that hold no fields and use only parameters and local variables — are trivially thread-safe. A method that computes a result purely from its arguments with no shared state can be called by any number of threads simultaneously without any synchronization. Stateless servlets and request handlers are the classic example: each request carries all the state it needs in the HttpServletRequest parameter. Thread confinement restricts access to a piece of mutable state to a single thread. If only one thread can ever access a field, no synchronization is needed — there is no concurrent access to protect against. Thread confinement can be enforced by design (a thread owns all access to a data structure by protocol), by ThreadLocal (each thread has its own copy of the variable), or by single-threaded executor services (all operations on a data structure are submitted to a single-threaded executor). Synchronization — via synchronized, volatile, atomic classes, or java.util.concurrent — is the fallback for shared mutable state that cannot be made immutable or confined. Among synchronization techniques, the preference order is: atomic classes (lock-free, high performance, limited to single-variable operations), volatile (no locks, but only for single variables where only visibility is needed, not atomicity of compound operations), synchronized (correct for all compound operations, higher overhead, potential for deadlock and starvation), and java.util.concurrent.locks (flexible, timed tryLock, multiple Conditions, explicit fairness control).
Java
// ── 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

Auditing a class for thread safety requires identifying every piece of mutable state the class owns, determining every thread that could access each piece of state, and verifying that every access is adequately protected. The audit has three phases: state identification, access enumeration, and protection verification. State identification: find all instance fields and static fields. Fields of primitive types, object references, and collections are all state. Fields that hold references to mutable objects inherit the mutability of those objects — a final reference to an ArrayList is mutable state because the ArrayList's contents can change. Fields inherited from superclasses are part of the class's state. Objects accessible through fields are part of the effective state if the class controls them. Access enumeration: for each field, find every location in the code that reads or writes it. This includes direct field access (this.field), access through an alias (a local variable holding a reference to this), access in inner classes, access in synchronized or unsynchronized blocks, and access from threads that the class creates or submits tasks to. Any method that is called by another thread is an access point. Protection verification: for each field, verify that every read and every write is protected by the same lock. If field balance is read in getBalance() (synchronized on this) and written in deposit() (synchronized on this), the lock is consistent and protection is complete. If balance is also read in an unsynchronized toString() method, that is a data race. Protection is only complete when every access — every read and every write across all methods, all threads, and all execution paths — uses the same lock. Common findings: public fields (always thread-unsafe unless volatile or immutable); methods that perform multiple operations assuming atomicity (if (!map.containsKey(k)) map.put(k, v) is not atomic even if map is a ConcurrentHashMap); synchronized methods that call unsynchronized methods on the same class; mutable state returned directly from a getter (the caller can modify the returned collection, bypassing all protection).
Java
// ── 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

Safe publication is the act of making an object reference available to threads other than the one that created it, in a way that ensures those threads see a fully initialized object. Unsafe publication occurs when a reference to an incompletely constructed object is published — for example, by storing this in a static field during construction, starting a thread from a constructor that refers to the outer class, or passing this to an event listener registered in the constructor. If another thread reads the reference before the constructor completes, it may see default values for fields that the constructor was supposed to initialize. Safe publication mechanisms: storing a reference in a volatile field; storing it in a field of a properly synchronized class (e.g., assigning to a ConcurrentHashMap); publishing it via a static initializer (the class loader guarantees that static initializers complete before any thread accesses the class); or using a final field (the JMM guarantees that final fields are visible after construction). Object escape is the premature publication of this from within a constructor. Any action in a constructor that makes this available to another thread — registering as a listener, starting a thread, calling a non-private non-final method — risks escape. The safe alternative is to construct the object fully, then publish it, or to use a factory method that performs registration after construction completes. The copy-on-read pattern provides a thread-safe read without a lock: instead of returning a reference to a mutable internal collection (which requires the caller to synchronize reads with writes), return a copy of the collection. The copy is taken under a lock; the caller receives their own snapshot that does not reflect subsequent changes. The tradeoff is the copy cost. For read-heavy workloads where copies are too expensive, CopyOnWriteArrayList maintains a snapshot automatically, at the cost of O(n) writes. The initialization-on-demand holder pattern provides thread-safe lazy singleton initialization without synchronization: the holder class is loaded only when Holder.INSTANCE is first accessed, and class loading is guaranteed by the JVM to be single-threaded and visible before the class reference returns.
Java
// ── 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);
    }
}

Related Topics in Multithreading

Process vs Thread
A process is an independent program in execution with its own isolated memory space, file handles, and system resources, managed by the operating system and separated from all other processes by strict boundaries. A thread is a unit of execution that lives inside a process, sharing that process's memory, heap, and resources with every other thread in the same process. Java programs run inside a JVM process; the JVM itself creates and manages threads, and every Java application starts with at least one thread — the main thread — with additional threads created by the JVM for garbage collection, JIT compilation, signal handling, and other runtime tasks. Understanding the distinction between processes and threads is the foundation for all concurrent programming in Java: it determines what is shared and what is isolated, what is fast and what is expensive, what fails independently and what fails together. This entry covers the OS-level and JVM-level model of processes and threads, the memory model that follows from the shared-versus-isolated distinction, the cost model for creation and context switching, failure isolation and its consequences, inter-process and inter-thread communication mechanisms, and the practical decision of when to use multiple processes versus multiple threads.
Thread Basics
A Java thread is an instance of java.lang.Thread that represents an independent path of execution within the JVM process. Every thread has a lifecycle — from creation through runnable, running, blocked, waiting, timed-waiting, and terminated states — and a set of properties including its name, priority, daemon status, thread group, and uncaught exception handler. The Java memory model specifies what visibility guarantees exist between threads and when writes by one thread are guaranteed to be visible to another. Thread scheduling is controlled by the OS scheduler subject to hints from the JVM via thread priority; the JVM does not provide real-time scheduling guarantees. This entry covers the complete thread lifecycle and its state machine, thread properties and how they affect scheduling and JVM shutdown, the happens-before relationship and why it matters for visibility, daemon threads and their relationship to JVM shutdown, thread interruption as a cooperative cancellation mechanism, and the methods on Thread that every Java developer must understand.
Creating Threads
Java provides three primary abstractions for defining the work a thread will execute: the Thread class itself (subclassed to override run()), the Runnable interface (a task with no return value and no checked exception), and the Callable interface (a task with a return value and a declared checked exception). Each represents a different contract between the task and the infrastructure that runs it. Thread subclassing couples the task definition to the execution mechanism and is the oldest and least flexible approach. Runnable decouples the task from the thread, allowing the same Runnable to be submitted to thread pools, scheduled executors, or wrapped in Thread objects. Callable extends that decoupling to include a return value and exception propagation, returning a Future that allows the caller to retrieve the result or handle exceptions asynchronously. Understanding all three — their contracts, their limitations, and when to use each — is the foundation of concurrent programming in Java before reaching for higher-level constructs.
Thread Lifecycle
The Java thread lifecycle is the complete sequence of states a thread passes through from the moment a Thread object is constructed to the moment its execution ends. Java defines six states in the Thread.State enum — NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, and TERMINATED — and the JVM transitions threads between these states in response to specific method calls, lock acquisitions, monitor notifications, timeouts, and exceptions. Each state has a precise meaning, a defined set of entry conditions, and a defined set of exit conditions. Understanding the lifecycle in full is prerequisite knowledge for diagnosing deadlocks, thread leaks, performance bottlenecks in thread dumps, and incorrect synchronization — all of which manifest as threads stuck in specific states. This entry covers every state in the lifecycle with its entry and exit conditions, all legal and illegal state transitions, how thread dumps represent each state, the interaction between lifecycle states and interruption, the effect of uncaught exceptions on lifecycle, and how to observe lifecycle transitions programmatically.