Java · Concurrency

Java Concurrency — Threads, Synchronization & the Executor Framework

5 min read Updated 2026-06-18

Practice Threads & Synchronization interview questions

Java concurrency and threads

Concurrency lets a program do many things at once, but it also opens the door to race conditions, deadlocks, and visibility bugs that only appear under load. Java gives you low-level threads and a rich high-level toolkit (executors, atomics, concurrent collections) to manage it. This guide builds from threads up to the executor framework and the memory model that makes it all correct.

Creating threads

Two basic approaches: extend Thread, or implement Runnable and pass it to a Thread. Runnable is preferred — it separates the task from the thread and works with executors.

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

start() creates a new thread and runs run() on it; calling run() directly just executes on the current thread — no concurrency. A thread can be started only once. In real code you rarely create threads by hand; you submit tasks to an ExecutorService.

Race conditions and synchronization

A race condition happens when multiple threads access shared mutable state and the result depends on timing. Operations that look atomic (count++) are actually read-modify-write sequences that can interleave and lose updates.

class Counter {
  private int count;
  synchronized void inc() { count++; }  // mutual exclusion + visibility
}

synchronized enforces mutual exclusion via an object's monitor lock — only one thread holds it at a time — and establishes a happens-before relationship guaranteeing visibility. An instance method locks on this; a static synchronized method locks on the Class object (so they use different locks — a common bug).

volatile vs synchronized

volatile guarantees visibility and ordering for a single variable — writes are immediately visible, with no caching or reordering — but not atomicity for compound operations.

volatile boolean running = true;  // flag — visibility is enough
volatile int count;
count++;                          // still a race — read-modify-write

Use volatile for flags published once (the classic stop-flag); use synchronized or atomics when you need atomic compound updates. synchronized adds mutual exclusion and can guard multiple variables, at the cost of blocking.

Deadlock, livelock, starvation

A deadlock is two+ threads each holding a lock the other needs, waiting forever. It requires four conditions (mutual exclusion, hold-and-wait, no preemption, circular wait); break one — usually by acquiring locks in a consistent global order or using tryLock with a timeout — to prevent it.

// Thread 1: lock A then B  |  Thread 2: lock B then A  -> deadlock

Starvation is a thread perpetually denied resources; livelock is threads actively running but never progressing (repeatedly responding to each other). Fixes include fairness policies, randomized back-off, and bounded retries.

Coordination: wait/notify and higher-level tools

wait()/notify()/notifyAll() coordinate threads on an object's monitor and must be called while holding its lock; always re-check the condition in a while loop (spurious wakeups). But you rarely need them — high-level utilities are safer:

// producer/consumer with a BlockingQueue — no manual wait/notify
BlockingQueue<Task> queue = new LinkedBlockingQueue<>(100);
queue.put(task);        // blocks if full
Task t = queue.take();  // blocks if empty

CountDownLatch (wait for N tasks, one-shot), CyclicBarrier (threads wait for each other, reusable), and Semaphore (limit concurrent access) cover most coordination needs. Note sleep() pauses without releasing locks, while wait() releases the lock.

The executor framework

An ExecutorService manages a pool of reusable threads and a task queue, so you submit work instead of creating threads. This caps thread count, reuses threads, and decouples submission from execution.

ExecutorService pool = Executors.newFixedThreadPool(4);
Future<Integer> f = pool.submit(() -> compute());
Integer result = f.get();   // blocks until done
pool.shutdown();            // graceful: finish queued tasks

Runnable returns nothing; Callable<V> returns a value and can throw; Future<V> is a handle whose get() blocks for the result. Because Future.get() blocks, prefer CompletableFuture for non-blocking composition:

CompletableFuture.supplyAsync(() -> fetchUser(id))
    .thenApply(User::name)
    .exceptionally(ex -> "fallback")
    .thenAccept(System.out::println);

Always shut a pool down (shutdown() then awaitTermination, falling back to shutdownNow()) — its non-daemon threads otherwise keep the JVM alive.

Lock-free tools: atomics and CAS

The java.util.concurrent.atomic classes (AtomicInteger, AtomicReference, …) provide thread-safe variables without locking, using compare-and-swap (CAS) — "if the value is still what I expect, replace it atomically." They outperform synchronized for simple counters under contention.

AtomicInteger count = new AtomicInteger();
count.incrementAndGet();         // atomic, lock-free
count.compareAndSet(5, 10);      // set to 10 only if currently 5

CAS is the basis of lock-free algorithms (no deadlock, better scalability), though it can spin under contention. For thread-confined state, ThreadLocal gives each thread its own copy — just remember to remove() it in thread pools to avoid leaks.

The Java Memory Model

The JMM defines when one thread's writes become visible to another, via the happens-before relationship. Key edges: unlocking a monitor -> subsequently locking it; a volatile write -> a subsequent read; Thread.start -> the thread's actions; a thread's actions -> another's join.

volatile boolean ready;
int data;
// Thread A: data = 42; ready = true;        // volatile write publishes data
// Thread B: if (ready) use(data);           // sees 42 — happens-before guarantees it

Without a happens-before relationship, a thread may never see another's writes — which is why volatile/synchronized exist. The classic double-checked locking singleton needs a volatile field precisely to prevent publishing a partially-constructed object.

Recap

Java concurrency starts with threads (prefer Runnable) and the hazards of shared mutable state — race conditions, deadlock, visibility bugs. synchronized gives mutual exclusion plus visibility; volatile gives visibility only; atomics/CAS give lock-free updates. Build on the high-level toolkit — executors and CompletableFuture for running tasks, BlockingQueue/latches/barriers for coordination, concurrent collections for shared data — and reason about correctness through happens-before in the memory model. Lean on those abstractions over raw threads and wait/notify, and concurrent Java becomes manageable.

Practice tests are coming soon

Get notified when interactive mock interviews and quizzes launch.