Concurrency & Threads

15 questions found

What are the two primary ways to create a thread in Java (extending Thread versus implementing Runnable), and which is generally preferred?

Beginner
You can either extend the Thread class directly (overriding run()) or implement the Runnable functional interface (defining run() and passing an instance to a new Thread) -- implementing Runnable is generally preferred since Java doesn't support multiple inheritance (extending Thread uses up your one available superclass slot, preventing your class from extending anything else), and it more cleanly separates the task's logic (Runnable) from the mechanism of running it as a thread (Thread), also enabling that same task logic to be submitted to a thread pool via ExecutorService without needing an actual Thread subclass at all.
// Preferred: implementing Runnable
Runnable task = () -> System.out.println("Running in: " + Thread.currentThread().getName());
Thread thread = new Thread(task);
thread.start();

// Less preferred: extending Thread directly
class MyThread extends Thread {
    @Override
    public void run() { System.out.println("Running"); }
}
Real-world example A codebase consistently uses Runnable (often as a lambda) submitted to an ExecutorService rather than manually creating and managing raw Thread objects, both for the flexibility Runnable provides and to benefit from a thread pool's reuse and resource management rather than creating a new OS thread for every single task.

Common follow-ups: What happens if you call run() directly instead of start() -- does it actually run on a new thread?;Why can a class only extend Thread OR implement Runnable, not benefit from both approaches simultaneously?

Background Tasks & Hosted Services;Design Patterns in Java

What is the synchronized keyword, and how does it use Java's intrinsic (monitor) locks to prevent race conditions on shared mutable state?

Intermediate
synchronized (applied to a method, or as a block with an explicit object reference) ensures only one thread at a time can execute the protected code while holding that object's intrinsic monitor lock, with other threads attempting to enter blocking until the lock is released -- this prevents race conditions where multiple threads read-modify-write shared state concurrently in an interleaved, inconsistent way, though it comes with the cost of serializing access (potentially becoming a contention bottleneck) and the risk of deadlock if locks are acquired in inconsistent orders across different code paths.
public class Counter {
    private int count = 0;

    public synchronized void increment() {  // locks 'this' for the duration of the method
        count++;  // without synchronization, this read-modify-write is NOT atomic and races under concurrency
    }

    public synchronized int getCount() {
        return count;
    }
}
Real-world example A shared counter incremented by multiple concurrent request-handling threads without synchronization exhibits a classic race condition ("lost updates", where the final count is lower than the actual number of increments due to interleaved read-modify-write operations), fixed by synchronizing the increment method to ensure each increment completes atomically.

Common follow-ups: What's the difference between synchronizing on 'this' versus a dedicated private lock object?;Why is the count++ operation not atomic even though it looks like a single statement?

Diagnostics & Performance;Design Patterns in Java

How does java.util.concurrent's ReentrantLock differ from the synchronized keyword, and what additional capabilities does it provide (tryLock, fairness, interruptible locking)?

Advanced
ReentrantLock provides everything synchronized offers (mutual exclusion, reentrancy -- a thread already holding the lock can re-acquire it without deadlocking itself) plus additional capabilities unavailable with synchronized: tryLock() (attempt to acquire without blocking indefinitely, optionally with a timeout, avoiding a thread getting stuck waiting forever), lockInterruptibly() (allows a blocked thread to be interrupted rather than waiting unconditionally), fairness (an optional constructor parameter approximating first-come-first-served lock acquisition order, reducing thread starvation at some throughput cost), and the ability to have multiple associated Condition objects for more granular wait/notify-style coordination than a single object's intrinsic wait()/notify().
private final ReentrantLock lock = new ReentrantLock();

public boolean tryUpdate() {
    if (lock.tryLock()) {  // non-blocking attempt, unlike synchronized which would wait indefinitely
        try {
            // critical section
            return true;
        } finally {
            lock.unlock();  // MUST be in finally -- unlike synchronized, unlock isn't automatic
        }
    }
    return false;  // couldn't acquire the lock, caller can decide to retry, skip, or fail fast
}
Real-world example A resource-management system uses ReentrantLock's tryLock() with a timeout to attempt acquiring a lock for a limited period before giving up and returning an error to the caller, avoiding the indefinite blocking that a plain synchronized block would impose if the lock happened to be held for an unexpectedly long time.

Common follow-ups: Why must lock.unlock() always be called in a finally block, unlike synchronized's automatic release?;When would you choose synchronized's simplicity over ReentrantLock's additional flexibility?

Background Tasks & Hosted Services;Error Handling

What is the ExecutorService framework, and why is it generally preferred over manually creating and managing individual Thread objects?

Intermediate
ExecutorService decouples task submission from the mechanics of thread creation, pooling, and lifecycle management -- a fixed or cached thread pool (created via Executors factory methods, or more explicitly via ThreadPoolExecutor for fine control) reuses a bounded set of worker threads across many submitted tasks, avoiding the overhead of creating a brand-new OS thread per task, providing built-in mechanisms for graceful shutdown, task result retrieval via Future, and protecting against unbounded thread creation that could otherwise exhaust system resources under heavy load.
ExecutorService executor = Executors.newFixedThreadPool(4);

Future<Integer> future = executor.submit(() -> {
    // some computation
    return 42;
});

try {
    Integer result = future.get(5, TimeUnit.SECONDS);  // blocks with a timeout
} catch (TimeoutException e) {
    future.cancel(true);
}

executor.shutdown();  // initiates graceful shutdown, letting in-flight tasks complete
Real-world example A web service handling incoming requests submits each request's processing work to a fixed-size ExecutorService thread pool rather than spawning a new Thread per request, bounding the maximum concurrent OS thread count and preventing resource exhaustion under a traffic spike that an unbounded thread-per-request model would risk.

Common follow-ups: What's the difference between Executors.newFixedThreadPool() and newCachedThreadPool(), and when is each appropriate?;Why do many teams recommend against using the Executors factory methods directly in production, preferring explicit ThreadPoolExecutor configuration?

Background Tasks & Hosted Services;Diagnostics & Performance

How do CompletableFuture's composition methods (thenApply, thenCompose, thenCombine) enable building asynchronous pipelines, and what's the difference between thenApply and thenCompose?

Advanced
CompletableFuture represents a value that will be available asynchronously in the future, with fluent composition methods letting you chain dependent asynchronous operations without manually blocking and coordinating threads yourself -- thenApply(Function) transforms the eventual result synchronously within the same stage (for a simple, non-async transformation), while thenCompose(Function returning another CompletableFuture) is used specifically when your transformation itself returns another asynchronous operation, flattening what would otherwise become a nested CompletableFuture<CompletableFuture<T>> into a single CompletableFuture<T> (analogous to flatMap versus map in Streams/Optional), and thenCombine merges two independent CompletableFutures once both complete.
CompletableFuture<User> userFuture = fetchUserAsync(userId);

// thenApply: simple synchronous transformation of the result
CompletableFuture<String> nameFuture = userFuture.thenApply(User::getName);

// thenCompose: chaining another ASYNC operation, avoiding nested CompletableFuture<CompletableFuture<T>>
CompletableFuture<List<Order>> ordersFuture = userFuture.thenCompose(user -> fetchOrdersAsync(user.getId()));

// thenCombine: merge two independent async operations once both complete
CompletableFuture<Report> reportFuture = userFuture.thenCombine(fetchInventoryAsync(), (user, inventory) -> buildReport(user, inventory));
Real-world example A microservice orchestrating calls to three downstream services (user info, order history, and inventory) uses thenCompose to sequentially chain dependent async calls (fetching orders requires the user ID from the first call) and thenCombine to run two independent calls concurrently, composing the entire multi-service workflow declaratively without any manual thread coordination.

Common follow-ups: What happens to exceptions thrown partway through a CompletableFuture chain, and how does exceptionally()/handle() address this?;What thread does each stage of a CompletableFuture chain actually execute on by default?

Background Tasks & Hosted Services;Error Handling

What causes a deadlock in multithreaded Java code, and what is the classic "lock ordering" strategy for preventing it?

Intermediate
A deadlock occurs when two or more threads each hold a lock the other needs while waiting to acquire the lock the other already holds, creating a circular wait with no thread able to proceed -- the classic prevention strategy is establishing and consistently following a global, agreed-upon lock acquisition order across all code paths (e.g., always acquiring lock A before lock B, never the reverse, regardless of which method or code path is involved), eliminating the possibility of circular waiting entirely since a consistent order can never form a cycle.
// DEADLOCK RISK: inconsistent lock ordering across two methods
public void transferAtoB() {
    synchronized (lockA) {
        synchronized (lockB) { /* transfer */ }
    }
}
public void transferBtoA() {
    synchronized (lockB) {  // acquires in OPPOSITE order -- deadlock risk if called concurrently!
        synchronized (lockA) { /* transfer */ }
    }
}

// FIX: always acquire in the same consistent global order (e.g., by object identity hash)
// regardless of the logical "direction" of the operation
Real-world example A banking application's fund transfer logic experiences an intermittent production deadlock traced to two methods acquiring the same pair of account locks in opposite order depending on transfer direction, fixed by establishing a consistent lock-ordering rule (e.g., always locking the account with the lower account ID first) regardless of transfer direction.

Common follow-ups: What other deadlock prevention strategies exist besides consistent lock ordering (like tryLock with timeout)?;How would you use a thread dump to diagnose an actual deadlock occurring in a running production application?

Diagnostics & Performance;Error Handling

How does the Java Memory Model's happens-before relationship formally define when changes made by one thread are guaranteed to be visible to another thread, and what role does the volatile keyword play?

Advanced
The Java Memory Model (JMM) defines a happens-before partial ordering of operations across threads: without an established happens-before relationship between a write in one thread and a subsequent read in another, the JMM makes no guarantee the reading thread will ever observe the write (due to CPU caching, instruction reordering by the compiler/JIT/CPU, or memory visibility effects) -- volatile establishes a happens-before relationship for reads/writes of that specific field (a write to a volatile field happens-before any subsequent read of that same field by another thread), and additionally prevents certain compiler/CPU reorderings around it, but notably does NOT make compound operations (like increment) atomic, only guaranteeing visibility and ordering, not mutual exclusion.
public class FlagExample {
    private volatile boolean running = true;  // volatile ensures visibility across threads

    public void stop() {
        running = false;  // write is guaranteed visible to other threads reading 'running'
    }

    public void doWork() {
        while (running) {  // without volatile, this loop might NEVER see the update, looping forever
            // work
        }
    }
}
Real-world example A background worker thread's while (running) loop fails to terminate when a separate control thread sets running = false, traced to the running field not being marked volatile, meaning the JVM/CPU was legally permitted to cache the field's value in the worker thread and never observe the other thread's write, resolved by adding the volatile modifier.

Common follow-ups: Why doesn't volatile make a compound operation like count++ atomic, and what's needed instead (AtomicInteger, synchronized)?;What specific compiler/CPU reordering optimizations does volatile prevent that could otherwise break multithreaded correctness?

Garbage Collection;JVM JRE & Memory

What are the atomic classes in java.util.concurrent.atomic (AtomicInteger, AtomicLong, AtomicReference), and how do they achieve thread-safe operations without traditional locking?

Intermediate
Atomic classes use hardware-level compare-and-swap (CAS) instructions (a single atomic CPU operation that conditionally updates a value only if it still matches an expected prior value) to implement thread-safe increment/update operations without acquiring a traditional lock -- this lock-free approach avoids the overhead and contention of synchronized/ReentrantLock for simple single-variable atomic operations, generally performing better under moderate contention, though CAS-based operations can spin-retry under very high contention (many threads simultaneously attempting to update the same atomic variable), at which point traditional locking might actually perform comparably or better.
private final AtomicInteger counter = new AtomicInteger(0);

public void increment() {
    counter.incrementAndGet();  // atomic, thread-safe, no explicit lock needed
}

public int compareAndUpdate(int expected, int newValue) {
    return counter.compareAndSet(expected, newValue) ? newValue : counter.get();
}
Real-world example A high-throughput metrics collection system tracking a shared request counter across many concurrent request-handling threads uses AtomicLong instead of a synchronized increment method, achieving better throughput under the metrics system's specific access pattern by avoiding traditional lock acquisition/release overhead entirely for this simple counting operation.

Common follow-ups: How does compare-and-swap (CAS) work at the CPU instruction level to achieve atomicity without a traditional lock?;Under what contention conditions might a CAS-based atomic actually perform worse than a traditional lock?

Diagnostics & Performance;JVM JRE & Memory

How would you use CountDownLatch, CyclicBarrier, and Semaphore for different thread coordination scenarios, and what distinguishes their use cases?

Advanced
CountDownLatch lets one or more threads wait until a set of operations being performed by other threads completes (a one-time, non-reusable count-down gate -- once it reaches zero, it stays open forever, useful for "wait until N things are done before proceeding"); CyclicBarrier makes a fixed number of threads wait for each other to all reach a common barrier point before any of them proceed (and, unlike CountDownLatch, is reusable/cyclic for repeated rounds of coordination, useful for phased parallel algorithms); Semaphore controls access to a limited number of permits, letting a bounded number of threads access a resource concurrently (like limiting concurrent connections to an external service), distinct from a simple lock which only ever allows exactly one thread through.
// CountDownLatch: wait for N worker threads to finish initialization before starting
CountDownLatch latch = new CountDownLatch(3);
for (int i = 0; i < 3; i++) {
    new Thread(() -> { initialize(); latch.countDown(); }).start();
}
latch.await();  // blocks main thread until all 3 have counted down

// Semaphore: limit concurrent access to at most 5 threads at a time
Semaphore semaphore = new Semaphore(5);
semaphore.acquire();
try { callLimitedResource(); } finally { semaphore.release(); }
Real-world example A service startup sequence uses CountDownLatch to block the main thread until all required subsystems (database connection pool, cache warm-up, config loading) have each independently signaled readiness on separate threads, while a separate rate-limiting layer uses a Semaphore to cap concurrent outbound calls to a fragile downstream API at exactly 10 at any given time.

Common follow-ups: Why is CyclicBarrier reusable across multiple 'rounds' while CountDownLatch is strictly one-time-use?;How does a Semaphore differ conceptually from a simple mutex/lock in terms of the guarantees it provides?

Rate Limiting;Background Tasks & Hosted Services

What is thread starvation, and how can thread priority misuse or unfair lock scheduling contribute to it?

Intermediate
Thread starvation occurs when a thread is perpetually denied access to a resource it needs to make progress, typically because other threads are repeatedly favored for scheduling or lock acquisition -- setting overly aggressive thread priorities (Thread.setPriority()) can starve lower-priority threads on some JVM/OS combinations (though thread priority is only a hint and behaves inconsistently across platforms, making it generally an unreliable and discouraged tool), and a non-fair lock (the default for both synchronized and ReentrantLock unless fairness is explicitly requested) can theoretically let some threads repeatedly "jump the queue" ahead of a thread that's been waiting longest, though non-fair locks are still generally preferred for their better overall throughput despite this theoretical starvation risk in adversarial scenarios.
// Non-fair (default) ReentrantLock -- higher throughput, but theoretically allows starvation
ReentrantLock lock = new ReentrantLock();  // fair = false by default

// Fair lock -- reduces (but doesn't eliminate) starvation risk, at the cost of some throughput
ReentrantLock fairLock = new ReentrantLock(true);
Real-world example A background task perpetually failing to acquire a heavily-contended lock under sustained load from many higher-frequency competing threads is diagnosed as a starvation issue, addressed by switching that specific lock to fair mode, trading a modest amount of overall throughput for a guarantee that no single thread gets perpetually starved.

Common follow-ups: Why is Thread priority considered an unreliable mechanism across different JVM/OS combinations?;What's the throughput cost of enabling fairness on a ReentrantLock, and when is that trade-off worthwhile?

Diagnostics & Performance;Rate Limiting

Showing 1–10 of 15