StackWise

Concurrency & Multithreading

Threads, executors, synchronized vs volatile, the memory model, locks, atomics, deadlocks and CompletableFuture.

18 shown · 0/18 known

Threads and executors

What is the difference between start() and run()?easy

start() asks the JVM to create a new thread that then calls run(). Calling run() directly just executes the method on the current thread like any other call — no concurrency. Calling start() twice throws IllegalThreadStateException.

Link
Runnable vs Callable?easy

Runnable.run() returns nothing and can’t throw checked exceptions. Callable<V>.call() returns a value and may throw Exception. Submitting a Callable to an ExecutorService yields a Future<V>; an exception thrown by the task surfaces as an ExecutionException from future.get().

Link
What are the thread states in Java?medium

NEW → RUNNABLE (running or ready) → possibly BLOCKED (waiting for a monitor), WAITING (wait, join, park without timeout) or TIMED_WAITING (sleep, timed wait/join) → back to RUNNABLE → TERMINATED. The JVM doesn’t distinguish “running” from “ready to run”.

Link
shutdown() vs shutdownNow()?easy

shutdown() stops accepting new tasks but lets queued and running tasks finish. shutdownNow() interrupts running tasks and returns the tasks still in the queue. The usual pattern: shutdown(), then awaitTermination(timeout), then shutdownNow() if it didn’t finish. Since Java 19, ExecutorService is AutoCloseable.

Link
How do you size a thread pool?medium
  • CPU-bound: about the number of cores (availableProcessors()), maybe +1.
  • I/O-bound platform threads: cores × (1 + waitTime / computeTime).
  • On Java 21, use virtual threads for I/O-bound work instead of tuning a big pool.

Also bound the queue and pick a rejection policy — Executors.newFixedThreadPool uses an unbounded queue that can grow until OutOfMemoryError.

Link
When does a ThreadPoolExecutor create threads beyond its core size?hard

Only when the queue is full. Submission order: if fewer than corePoolSize threads → start a thread; else try to enqueue; if the queue is full and fewer than maximumPoolSize threads → start a thread; else reject. With an unbounded queue, the pool never grows past the core size — maximumPoolSize is ignored.

Link

Memory model and locking

synchronized vs volatile?medium

synchronized provides mutual exclusion and visibility: one thread at a time in the critical section, and everything written before the unlock is visible to the next locker. volatile provides visibility and ordering for a single variable but no atomicity for compound actions. Use volatile for flags and safely published references; use locks or atomics for read-modify-write.

Link
Is count++ on a volatile int thread-safe?medium

No. count++ is read, add, write — two threads can read the same value and both write n + 1, losing an update. volatile only guarantees each read sees the latest write. Use AtomicInteger.incrementAndGet(), LongAdder, or a lock.

Link
What is the happens-before relationship?hard

The JMM’s rule for when one thread’s write is guaranteed visible to another’s read. Key edges: unlocking a monitor → later lock of the same monitor; a volatile write → later reads of it; Thread.start() → actions in the new thread; a thread’s actions → another thread’s successful join(); completing a Future → get(). Without a happens-before path you have a data race and may read stale values.

Link
Why does double-checked locking need a volatile field?hard

Without volatile, the write of the reference can be reordered before the constructor’s field writes, so another thread may see a non-null but partially constructed object. volatile forbids that reordering and publishes the fully constructed object. Simpler alternatives: the initialization-on-demand holder idiom or an enum.

Link
What is a deadlock and how do you prevent it?medium

Threads waiting on each other’s locks in a cycle (T1 holds A, wants B; T2 holds B, wants A). Prevention: acquire locks in a global order; use tryLock(timeout) and back off; minimise lock scope and never call unknown code while holding a lock; prefer higher-level concurrency utilities. Diagnose with a thread dump (jstack, jcmd Thread.print) — the JVM reports detected deadlocks.

Link
synchronized vs ReentrantLock?medium

Both are reentrant and give the same memory semantics. ReentrantLock adds tryLock() (with timeout), lockInterruptibly(), optional fairness, and multiple Conditions — but you must unlock() in finally. synchronized is simpler and can’t be forgotten. On Java 21, ReentrantLock also avoids virtual-thread pinning around blocking calls.

Link
How does AtomicInteger work without locks?medium

Via compare-and-swap (CAS), a CPU instruction that updates a value only if it still equals the expected one. incrementAndGet loops: read, compute, CAS; retry if another thread won. It’s lock-free — threads never block — but under heavy contention retries spin, which is why LongAdder (striped cells) is faster for hot counters.

Link
CountDownLatch vs CyclicBarrier vs Semaphore?medium
  • CountDownLatch — one-shot; threads wait until a count reaches zero (e.g. wait for N services to start).
  • CyclicBarrier — reusable rendezvous; a fixed number of threads wait for each other at a point, phase after phase.
  • Semaphore — N permits limit concurrent access to a resource (connection limits, throttling).
Link
What is the producer-consumer problem and how do you solve it in Java?easy

Producers add work and consumers remove it at different rates. Use a BlockingQueue — put blocks when a bounded queue is full (back-pressure) and take blocks when empty — instead of hand-written wait/notify. Shut consumers down with a poison-pill item or by interrupting them.

Link

CompletableFuture

thenApply vs thenCompose vs thenCombine?medium
  • thenApply(fn) — transform the result (map).
  • thenCompose(fn) — chain a dependent async call whose function returns a CompletableFuture (flatMap); avoids nested futures.
  • thenCombine(other, fn) — combine two independent futures when both complete.
Link
How do you handle exceptions in a CompletableFuture chain?medium

An exception completes the stage exceptionally and skips dependent stages until a handler: exceptionally(ex -> fallback), handle((result, ex) -> ...) (both success and failure), or whenComplete (observe without changing the result). Exceptions arrive wrapped in CompletionException — unwrap with getCause(). Also consider orTimeout / completeOnTimeout.

Link
Why should you pass a custom executor to supplyAsync?hard

Without one, async stages run on ForkJoinPool.commonPool(), sized for CPU work (cores - 1) and shared by the whole JVM, including parallel streams. Blocking I/O there starves everything else. Pass a dedicated executor — on Java 21, Executors.newVirtualThreadPerTaskExecutor() is a good fit for I/O.

Link