Multithreading & Concurrency

Difficulty

A process is an independently executing program with its own isolated memory space, file handles, and OS resources — processes don't share memory directly and communicate via IPC (pipes, sockets, shared files). A thread is a unit of execution within a process; all threads in a process share the same heap and static memory, which is what makes concurrent access to shared data a concern (and also what makes inter-thread communication cheap compared to inter-process communication).

Three ways to create a task that runs on a thread:

1. Extend Thread, override run():

class MyThread extends Thread {
    public void run() { System.out.println("running"); }
}
new MyThread().start(); // start(), not run() — run() alone just calls the method normally

2. Implement Runnable (preferred — doesn't burn your one shot at extending a class, and separates "what to run" from "how it runs"):

Runnable task = () -> System.out.println("running");
new Thread(task).start();

3. Implement Callable<V> — like Runnable but can return a value and throw checked exceptions, typically submitted to an ExecutorService rather than run directly on a raw Thread:

Callable<Integer> task = () -> { return compute(); };
Future<Integer> future = executor.submit(task);
int result = future.get(); // blocks until done, rethrows checked exceptions wrapped

Best practice: prefer Runnable/Callable submitted to an ExecutorService over manually managing raw Thread objects — it decouples task logic from thread lifecycle/pooling and is far easier to tune and reason about at scale.

Thread.State (queryable via thread.getState()) defines six states:

  1. NEW: the Thread object has been created but start() hasn't been called yet.
  2. RUNNABLE: the thread is executing, or is eligible to execute and waiting for CPU time from the OS scheduler — Java doesn't distinguish "actually running" from "ready to run" at the language level; both map to RUNNABLE.
  3. BLOCKED: the thread is waiting to acquire a monitor lock to enter a synchronized block/method that another thread currently holds.
  4. WAITING: the thread is waiting indefinitely for another thread to perform a specific action — e.g., it called Object.wait() (no timeout), Thread.join() (no timeout), or LockSupport.park().
  5. TIMED_WAITING: like WAITING, but bounded by a timeout — e.g., Thread.sleep(ms), Object.wait(timeout), Thread.join(timeout).
  6. TERMINATED: the thread's run() method has completed (normally or via an uncaught exception); it cannot be restarted.
Thread t = new Thread(() -> { try { Thread.sleep(1000); } catch (InterruptedException e) {} });
t.getState(); // NEW
t.start();
t.getState(); // RUNNABLE (or briefly TIMED_WAITING once sleep() kicks in)
t.join();
t.getState(); // TERMINATED

A common interview follow-up: BLOCKED vs WAITINGBLOCKED specifically means contending for a lock to enter a synchronized region; WAITING means the thread voluntarily gave up execution and is waiting for a signal (notify()/notifyAll()) or another thread to finish, having already been inside (or without needing) a lock.

Related Resources

synchronized provides mutual exclusion: only one thread can hold a given object's intrinsic lock (monitor) at a time, so code guarded by the same lock never runs concurrently across threads. It also establishes a happens-before relationship — changes made by a thread before releasing the lock are guaranteed visible to the next thread that acquires the same lock, which matters as much as the mutual exclusion itself.

Synchronized instance method — locks on this (the current object):

class Counter {
    private int count;
    synchronized void increment() { count++; } // locks on `this`
}

Synchronized static method — locks on the Class object itself (shared across all instances):

class Registry {
    static synchronized void register(String name) { /* locks on Registry.class */ }
}

Synchronized block — locks on any explicit object you choose, letting you scope the critical section as narrowly as possible (reducing how long the lock is held, and thus contention):

class Cache {
    private final Object lock = new Object();
    private Map<String, String> data = new HashMap<>();

    void put(String k, String v) {
        // ... some non-critical work here, unsynchronized ...
        synchronized (lock) {
            data.put(k, v); // only this part needs the lock
        }
    }
}

Why prefer blocks over whole-method synchronization: synchronizing an entire method locks for its whole duration, even parts that don't touch shared state — a synchronized block using a dedicated private lock object minimizes contention and avoids accidentally exposing your lock object to external code that could lock on it unexpectedly (a risk when synchronizing on this in a public API).

Both relate to thread visibility of shared state, but solve different problems:

volatile guarantees:

  • Visibility: a write to a volatile field is immediately visible to all other threads' subsequent reads (it's flushed to/read from main memory rather than a per-CPU cache, roughly speaking).
  • Ordering: prevents the compiler/JIT/CPU from reordering instructions around the volatile access in ways that would break the visibility guarantee.

It does not guarantee atomicity for compound operations:

volatile int counter = 0;
counter++; // NOT atomic — read, increment, write are 3 separate steps
           // two threads can interleave and lose an increment

synchronized guarantees both visibility (via happens-before on lock acquire/release) and atomicity/mutual exclusion — only one thread executes the guarded block at a time, so compound operations like counter++ are safe if wrapped in it:

synchronized (lock) {
    counter++; // atomic with respect to other threads using the same lock
}

When volatile is enough: simple flags or single-write-many-read state where you don't need atomic compound updates — e.g., a volatile boolean shutdownRequested flag checked by a worker loop.

When you need synchronized (or java.util.concurrent.atomic classes): anything involving read-modify-write sequences (counters, "check-then-act" logic) where multiple threads might interleave mid-operation.

volatile is also cheaper — it has no lock acquisition/contention overhead, just a memory barrier — but that only matters if it actually fits the visibility-only use case.

Deadlock happens when two (or more) threads are each waiting for a resource the other holds, so none of them can ever proceed:

Object lockA = new Object(), lockB = new Object();

// Thread 1
synchronized (lockA) {
    Thread.sleep(100);
    synchronized (lockB) { /* ... */ } // waits for lockB, held by Thread 2
}

// Thread 2
synchronized (lockB) {
    Thread.sleep(100);
    synchronized (lockA) { /* ... */ } // waits for lockA, held by Thread 1
}
// Both threads block forever — classic deadlock

The four classic necessary conditions (Coffman conditions) are: mutual exclusion, hold-and-wait, no preemption, and circular wait — breaking any one prevents deadlock.

Prevention strategies:

  1. Fixed lock ordering: always acquire multiple locks in the same globally-agreed order across all threads (e.g., by a stable ID). If both threads above always locked lockA before lockB, deadlock becomes impossible — this breaks the "circular wait" condition.
  2. Lock timeouts: use Lock.tryLock(timeout) from java.util.concurrent.locks instead of an unconditional blocking acquire — if a thread can't get the second lock within the timeout, it backs off, releases what it holds, and retries, breaking "hold-and-wait."
  3. Minimize lock scope and count: hold locks for as short a time as possible, and avoid nesting multiple locks when a single, coarser lock (or a lock-free structure) would do.
  4. Prefer higher-level concurrency utilities: java.util.concurrent collections (ConcurrentHashMap), java.util.concurrent.atomic classes, and executor-based designs often avoid the need for manual multi-lock coordination entirely.

Detecting an existing deadlock in production: a thread dump (jstack <pid>) explicitly reports "Found one Java-level deadlock" with the involved threads and locks.