Chaturmind
LearnDSASystem DesignInterview PrepDevOpsEngineering GrowthBlog
Start learning
Chaturmind

Structured learning paths for engineers who want to go deep. Written by practitioners.

Learn

  • Java
  • DSA
  • System Design
  • Spring Boot
  • AI / ML
  • DevOps
  • Engineering Growth
  • Java Interview Prep

Company

  • Blog
  • Contact

Legal

  • Privacy Policy
  • Terms of Service

© 2026 Chaturmind. All rights reserved.

Built for engineers who want to go deep.


← Java Interview Prep: 8+ Years (Senior & Lead)

Expert Core Java

  • Tricky Java Output, Operators & OOP Edge Cases — Interview Questions
  • Tricky Exceptions, Memory & Keyword Questions — Interview Questions
  • Classic Java Language Questions, Senior-Grade Answers — Interview Questions
  • Classic Collections, Threads & JDK APIs, Senior-Grade Answers — Interview Questions
  • Reflection, Dynamic Proxies, final & Modern OOP Design — Interview Questions

JVM Internals & Performance

  • Class Loading, Bytecode & Object Layout — Interview Questions
  • JIT Compilation & Runtime Optimisations — Interview Questions
  • Garbage Collectors Deep Dive — Interview Questions
  • JVM Tuning, GC Logs & Memory Footprint — Interview Questions
  • Memory Leaks, OutOfMemoryErrors & Profiling Tools — Interview Questions
  • Modules, Agents & Advanced JVM APIs — Interview Questions

Collections & Concurrency at Scale

  • Collections Internals & Complexity — Interview Questions
  • Iterators, Comparators & Ordering Contracts — Interview Questions
  • Concurrent Collections, Queues & Lock-Free Structures — Interview Questions
  • Threads, Executors & ForkJoin Internals — Interview Questions
  • Locks, Atomics, CAS & Synchronizers — Interview Questions
  • Java Memory Model, volatile, Fences & ThreadLocal — Interview Questions
  • Deadlock, Livelock, Starvation & Concurrent Design — Interview Questions
  • CompletableFuture, Parallel Streams & Non-Blocking I/O — Interview Questions

Modern Java (8 to 21+)

  • Lambdas & Functional Interfaces Internals — Interview Questions
  • Streams & Collectors Deep Dive — Interview Questions
  • Optional & Interface Default/Static Methods — Interview Questions
  • Java 9–25 Features & Virtual Threads — Interview Questions

Design Patterns, SOLID & Clean Code

  • Design Pattern Trade-offs & Combinations — Interview Questions
  • SOLID, Clean Code & Anti-Patterns — Interview Questions

Spring & Spring Boot Internals

  • IoC, Dependency Injection & Bean Lifecycle Internals — Interview Questions
  • Spring AOP, Proxies & @Async Internals — Interview Questions
  • Spring Configuration, Auto-Configuration & Custom Starters — Interview Questions
  • Spring MVC & REST Internals, Exception Frameworks — Interview Questions
  • Spring Security Advanced Internals — Interview Questions
  • Spring WebFlux, Reactor & R2DBC — Interview Questions
  • Spring Cloud, Observability & Distributed Tracing — Interview Questions
  • Spring Boot 3, Native Images & Production Scenarios — Interview Questions

JPA, Hibernate & Databases at Scale

  • Spring Data JPA — Queries, Projections, Custom Repositories & Locking — Interview Questions
  • JPA Entity Mapping, Associations & Cascades — Interview Questions
  • JPQL vs Native Queries in Depth — Interview Questions
  • Hibernate Caching — First-Level, Second-Level & Query Cache — Interview Questions
  • Lazy vs Eager Loading, LazyInitializationException & N+1 — Interview Questions
  • JPA Transactions, Propagation, Isolation & Dirty Checking — Interview Questions
  • SQL vs NoSQL, Indexing & Query Tuning — Interview Questions
  • Database Scaling, Replication, Pooling & Consistency Models — Interview Questions
  • Redis, Search, Time-Series, CDC & Transactional Data Modelling — Interview Questions

Testing Strategy & API Design

  • Spring Boot Test Slices, Context & Test Strategy — Interview Questions
  • Testing Web, Persistence, Security, Async & Messaging in Spring Boot — Interview Questions
  • JUnit 5 & Mockito, Advanced — Interview Questions
Chaturmind
← Java Interview Prep: 8+ Years (Senior & Lead)

Expert Core Java

  • Tricky Java Output, Operators & OOP Edge Cases — Interview Questions
  • Tricky Exceptions, Memory & Keyword Questions — Interview Questions
  • Classic Java Language Questions, Senior-Grade Answers — Interview Questions
  • Classic Collections, Threads & JDK APIs, Senior-Grade Answers — Interview Questions
  • Reflection, Dynamic Proxies, final & Modern OOP Design — Interview Questions

JVM Internals & Performance

  • Class Loading, Bytecode & Object Layout — Interview Questions
  • JIT Compilation & Runtime Optimisations — Interview Questions
  • Garbage Collectors Deep Dive — Interview Questions
  • JVM Tuning, GC Logs & Memory Footprint — Interview Questions
  • Memory Leaks, OutOfMemoryErrors & Profiling Tools — Interview Questions
  • Modules, Agents & Advanced JVM APIs — Interview Questions

Collections & Concurrency at Scale

  • Collections Internals & Complexity — Interview Questions
  • Iterators, Comparators & Ordering Contracts — Interview Questions
  • Concurrent Collections, Queues & Lock-Free Structures — Interview Questions
  • Threads, Executors & ForkJoin Internals — Interview Questions
  • Locks, Atomics, CAS & Synchronizers — Interview Questions
  • Java Memory Model, volatile, Fences & ThreadLocal — Interview Questions
  • Deadlock, Livelock, Starvation & Concurrent Design — Interview Questions
  • CompletableFuture, Parallel Streams & Non-Blocking I/O — Interview Questions

Modern Java (8 to 21+)

  • Lambdas & Functional Interfaces Internals — Interview Questions
  • Streams & Collectors Deep Dive — Interview Questions
  • Optional & Interface Default/Static Methods — Interview Questions
  • Java 9–25 Features & Virtual Threads — Interview Questions

Design Patterns, SOLID & Clean Code

  • Design Pattern Trade-offs & Combinations — Interview Questions
  • SOLID, Clean Code & Anti-Patterns — Interview Questions

Spring & Spring Boot Internals

  • IoC, Dependency Injection & Bean Lifecycle Internals — Interview Questions
  • Spring AOP, Proxies & @Async Internals — Interview Questions
  • Spring Configuration, Auto-Configuration & Custom Starters — Interview Questions
  • Spring MVC & REST Internals, Exception Frameworks — Interview Questions
  • Spring Security Advanced Internals — Interview Questions
  • Spring WebFlux, Reactor & R2DBC — Interview Questions
  • Spring Cloud, Observability & Distributed Tracing — Interview Questions
  • Spring Boot 3, Native Images & Production Scenarios — Interview Questions

JPA, Hibernate & Databases at Scale

  • Spring Data JPA — Queries, Projections, Custom Repositories & Locking — Interview Questions
  • JPA Entity Mapping, Associations & Cascades — Interview Questions
  • JPQL vs Native Queries in Depth — Interview Questions
  • Hibernate Caching — First-Level, Second-Level & Query Cache — Interview Questions
  • Lazy vs Eager Loading, LazyInitializationException & N+1 — Interview Questions
  • JPA Transactions, Propagation, Isolation & Dirty Checking — Interview Questions
  • SQL vs NoSQL, Indexing & Query Tuning — Interview Questions
  • Database Scaling, Replication, Pooling & Consistency Models — Interview Questions
  • Redis, Search, Time-Series, CDC & Transactional Data Modelling — Interview Questions

Testing Strategy & API Design

  • Spring Boot Test Slices, Context & Test Strategy — Interview Questions
  • Testing Web, Persistence, Security, Async & Messaging in Spring Boot — Interview Questions
  • JUnit 5 & Mockito, Advanced — Interview Questions
HomeLearnJava Interview PrepJava Interview Prep: 8+ Years (Senior & Lead)Collections & Concurrency at Scale
✓ FreeAdvanced· 12 min read

Threads, Executors & ForkJoin Internals — Interview Questions

Thread vs Runnable vs Callable, sharing one Runnable across threads, Future.get vs CompletableFuture.join, interruption semantics, yield vs sleep, Executor vs ExecutorService vs ThreadPoolExecutor, how newFixedThreadPool works, what happens when tasks exceed threads, corePoolSize/maximumPoolSize/keepAliveTime interplay (and the queue-before-grow surprise), worker lifecycle and pool states, how the executor tracks threads, graceful vs forced shutdown, ForkJoinPool and work stealing, and how parallel streams use the common pool.

Published September 25, 2026


How to use this lesson

Executor questions check whether you can size and operate thread pools in production:

  • the exact ThreadPoolExecutor decision algorithm;
  • why unbounded queues are dangerous;
  • how shutdown and interruption really work;
  • when to use ForkJoin, or virtual threads, instead.

Q1. What's the difference between Thread, Runnable and Callable? Which should you prefer?

Short answer:

  • Thread is the execution mechanism: an OS or virtual thread, with a lifecycle.
  • Runnable is a task returning nothing: void run(), with no checked exceptions.
  • Callable<V> is a task that returns a value and may throw checked exceptions: V call() throws Exception.

Prefer tasks (Runnable/Callable) submitted to an executor, rather than subclassing or creating Threads yourself:

  • the task is decoupled from the threading policy (pool size, virtual threads, scheduling);
  • results and exceptions come back through Future/CompletableFuture;
  • your class can still extend something else.

Raw new Thread(...) is fine for rare, long-lived dedicated threads, or tests. For many concurrent I/O tasks, use Executors.newVirtualThreadPerTaskExecutor() (Java 21).

Learn it in depth → ExecutorService & Thread Pools

Q2. What happens when you start the same Runnable in two threads? Can one Runnable be shared by several threads?

Short answer: Yes. new Thread(task).start() twice runs task.run() concurrently, in two threads, on the same Runnable object. Output order is nondeterministic. The implication: any instance fields of that Runnable are shared state. Counters or buffers in it are subject to races, so they must be thread-safe, or the task must be stateless (keeping state in local variables). A shared, stateless task is fine, and common: the same Runnable submitted many times to a pool.

class Counter implements Runnable {
    private int count;                                 // shared between threads, so racy
    public void run() { for (int i = 0; i < 1000; i++) count++; }
}
Counter c = new Counter();
Thread a = new Thread(c), b = new Thread(c);
a.start(); b.start(); a.join(); b.join();
System.out.println(c.count);                           // usually < 2000 (lost updates). Use AtomicInteger/LongAdder

Q3. How is Future.get() different from join() on a CompletableFuture?

Short answer: Both block until the result is ready. The difference is in exceptions:

  • get() (from Future) throws checked InterruptedException and ExecutionException (wrapping the cause). It has a timeout overload, get(timeout, unit), and it responds to interruption.
  • join() (on CompletableFuture) throws the unchecked CompletionException (wrapping the cause), or CancellationException, so it's convenient in lambdas and streams. It has no timeout, and it doesn't throw InterruptedException.

Key points to cover:

  • Prefer non-blocking composition (thenApply, thenCompose, orTimeout) over either.
  • Block only at the edges: tests, a main method, or a virtual thread where blocking is cheap.
  • Java 19 added resultNow()/exceptionNow()/state() for completed futures.

Q4. How do you interrupt a thread, and what does Thread.interrupt() actually do?

Short answer: interrupt() doesn't stop a thread. It sets the target's interrupt status flag, and:

  • if the thread is blocked in an interruptible call (sleep, wait, join, BlockingQueue.take, Lock.lockInterruptibly, interruptible NIO channels), that call throws InterruptedException (or ClosedByInterruptException), and clears the flag;
  • otherwise, only the flag is set. The code must check it cooperatively (Thread.currentThread().isInterrupted()).

Correct handling:

  • propagate the InterruptedException;
  • or restore the flag (Thread.currentThread().interrupt()) and exit.

Never swallow it. Blocking socket I/O (classic java.io) and synchronized lock acquisition aren't interruptible.

public void run() {
    while (!Thread.currentThread().isInterrupted()) {
        try {
            Job job = queue.take();                     // wakes up with InterruptedException on interrupt
            process(job);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();         // restore, then exit the loop cleanly
            return;
        }
    }
}

Key points to cover:

  • Future.cancel(true) and ExecutorService.shutdownNow() work through interruption.
  • Thread.stop() is deprecated, and since Java 20 it throws UnsupportedOperationException.

Q5. What's the difference between Thread.yield() and Thread.sleep()?

Short answer:

  • sleep(ms) guarantees the thread stops running for at least the given time (subject to timer granularity), moving it to TIMED_WAITING. It's interruptible, and keeps any locks it holds.
  • yield() is a hint to the scheduler that the thread is willing to give up the CPU. It stays RUNNABLE, and may be scheduled again immediately. Its behaviour is platform-dependent, and it's rarely useful in application code. Use it only in spin-wait loops (Thread.onSpinWait(), Java 9, is the better hint for spin loops).

Neither is a correct tool for coordination between threads. Use latches, conditions or queues.

Q6. What's the difference between Executor, ExecutorService and ThreadPoolExecutor?

Short answer:

  • Executor: the minimal interface, void execute(Runnable). It decouples task submission from execution.
  • ExecutorService: extends it with lifecycle management (shutdown, shutdownNow, awaitTermination, close() since Java 19, which makes it usable with try-with-resources) and results (submit returning a Future, invokeAll, invokeAny). ScheduledExecutorService adds scheduling.
  • ThreadPoolExecutor: the main configurable implementation: core and maximum pool size, keep-alive time, work queue, ThreadFactory, RejectedExecutionHandler, and hooks (beforeExecute, afterExecute). Most of the Executors.* factory methods return one.

Q7. How does Executors.newFixedThreadPool(n) work internally? What happens when you submit more tasks than there are threads?

Short answer: It creates new ThreadPoolExecutor(n, n, 0L, MILLISECONDS, new LinkedBlockingQueue<>()):

  • core = max = n, so threads are created on demand, up to n, and never time out;
  • the queue is an unbounded LinkedBlockingQueue.

When more tasks arrive than there are threads, the extras wait in the queue, in FIFO order. The danger is that the queue is unbounded: under sustained overload it grows without limit, so latency explodes, and eventually you get an OutOfMemoryError. Nothing is ever rejected. The same applies to newSingleThreadExecutor.

Production practice: build a ThreadPoolExecutor explicitly with a bounded queue, a named ThreadFactory, and a deliberate rejection policy, and expose metrics (Micrometer's ExecutorServiceMetrics).

Q8. How do corePoolSize, maximumPoolSize and keepAliveTime interact in ThreadPoolExecutor?

Short answer: On execute(task), the pool follows this algorithm:

  1. If running threads < corePoolSize, it starts a new thread, even if others are idle.
  2. Otherwise, it offers the task to the queue.
  3. Only if the queue is full, and the number of threads is below maximumPoolSize, does it start a non-core thread.
  4. Otherwise, it rejects the task (through the RejectedExecutionHandler).
  5. Threads above the core size that stay idle for keepAliveTime are terminated. The core threads can time out too, with allowCoreThreadTimeOut(true).

Common trap: with an unbounded queue, step 3 never happens. maximumPoolSize is ignored, and the pool never grows beyond its core size. People configure core=10, max=100 with a LinkedBlockingQueue, and wonder why only 10 threads ever run. Use a bounded queue, or a SynchronousQueue (a direct hand-off, as newCachedThreadPool does).

ThreadPoolExecutor pool = new ThreadPoolExecutor(
        16, 64, 30, TimeUnit.SECONDS,
        new ArrayBlockingQueue<>(500),                              // bounded: back-pressure
        Thread.ofPlatform().name("pricing-", 0).factory(),
        new ThreadPoolExecutor.CallerRunsPolicy());                 // throttles submitters when saturated

Q9. When is a pool thread killed in ThreadPoolExecutor? What are the pool's states, and how does it track its threads?

Short answer:

  • Workers terminate when:
    • they're idle longer than keepAliveTime and the pool has more than corePoolSize threads (or core timeout is allowed);
    • the pool shuts down and the queue is drained;
    • a task throws an uncaught exception: that worker dies, and the pool replaces it with a new one (with execute; with submit, the exception is captured in the Future, and the worker survives).
  • How it tracks threads: each thread is a Worker (it wraps the thread, and is itself a small lock), held in a HashSet<Worker> guarded by mainLock. An atomic ctl integer packs the run state (3 bits) and the worker count (29 bits). The monitoring methods read this state: getPoolSize(), getActiveCount() (workers currently holding their lock, meaning running a task), getCompletedTaskCount(), getQueue().size() and getLargestPoolSize(). There's no "dead thread" count, because dead workers are removed and replaced.
  • Run states:
    • RUNNING: accepts and processes tasks.
    • SHUTDOWN: no new tasks, but the queue keeps draining.
    • STOP: shutdownNow(): no new tasks, the queue is abandoned, and workers are interrupted.
    • TIDYING: all workers are gone, and the terminated() hook runs.
    • TERMINATED.

Common trap: the source says the executor "first checks if there are idle threads". While below the core size, it starts a new thread regardless of idle ones. After that, it queues before growing.

Q10. How do you shut down a thread pool gracefully, and forcefully?

Short answer:

void shutdownGracefully(ExecutorService pool) {
    pool.shutdown();                                        // stop accepting; finish queued and running tasks
    try {
        if (!pool.awaitTermination(30, TimeUnit.SECONDS)) {
            List<Runnable> dropped = pool.shutdownNow();    // interrupt running tasks; return the never-started ones
            log.warn("Forced shutdown, {} tasks dropped", dropped.size());
            if (!pool.awaitTermination(10, TimeUnit.SECONDS)) log.error("Pool did not terminate");
        }
    } catch (InterruptedException e) {
        pool.shutdownNow();
        Thread.currentThread().interrupt();
    }
}

Key points to cover:

  • shutdownNow() relies on interruption. Tasks that ignore interrupts keep running.
  • Since Java 19, ExecutorService implements AutoCloseable: close() means shutdown, then wait. Use it with try-with-resources for scoped pools.
  • In Spring, ThreadPoolTaskExecutor with setWaitForTasksToCompleteOnShutdown(true) and setAwaitTerminationSeconds(...) handles this during context shutdown.

Q11. How does ForkJoinPool differ from ThreadPoolExecutor? When is it the better choice? How does the Fork/Join framework work?

Short answer:

  • ForkJoinPool:
    • built for divide-and-conquer, CPU-bound work;
    • tasks (RecursiveTask/RecursiveAction) fork subtasks and join their results;
    • each worker has its own double-ended queue, and idle workers steal from others (see the next question);
    • join() doesn't simply block: the joining worker helps execute other tasks while it waits, so recursion doesn't exhaust the threads;
    • parallelism defaults to the number of CPU cores.
  • ThreadPoolExecutor: one shared queue. Suited to independent tasks (requests, jobs, I/O).

Prefer ForkJoin for recursive algorithms (parallel merge sort, tree or graph processing, large array reductions), and implicitly through parallel streams. It's also the carrier scheduler for virtual threads. Avoid blocking I/O in it (use ManagedBlocker if you must).

class SumTask extends RecursiveTask<Long> {
    private final long[] a; private final int lo, hi;
    SumTask(long[] a, int lo, int hi) { this.a = a; this.lo = lo; this.hi = hi; }
    protected Long compute() {
        if (hi - lo <= 10_000) { long s = 0; for (int i = lo; i < hi; i++) s += a[i]; return s; }
        int mid = (lo + hi) >>> 1;
        SumTask left = new SumTask(a, lo, mid);
        left.fork();                                        // push onto this worker's deque
        long right = new SumTask(a, mid, hi).compute();     // work on the other half directly
        return right + left.join();
    }
}
long total = ForkJoinPool.commonPool().invoke(new SumTask(data, 0, data.length));

Learn it in depth → Fork/Join Framework

Q12. What is work stealing in ForkJoinPool?

Short answer: Each worker thread owns a deque of tasks:

  • it pushes and pops its own tasks at the head (LIFO), which gives good cache locality, and works on the smallest, most recent subtasks first;
  • an idle worker steals from the tail (FIFO) of another worker's deque, taking the oldest, largest tasks. Those split into more work, so there are fewer steals.

Owners and thieves work at opposite ends, so contention is minimal (only CAS operations on steals). The result is automatic load balancing of irregular recursive work, without a central queue bottleneck.

Q13. How many threads do parallel streams use, and how do they work internally?

Short answer:

  • Parallel streams run in the common ForkJoinPool, whose parallelism is availableProcessors() - 1, plus the calling thread, which also participates. So you get about one thread per core in total.
  • You can change it with -Djava.util.concurrent.ForkJoinPool.common.parallelism=N (JVM-wide), or run the stream inside a custom ForkJoinPool (pool.submit(() -> list.parallelStream()...).get()). That second trick works, but is undocumented behaviour.
  • Internally:
    1. The source's Spliterator recursively splits the data (trySplit). ArrayList, arrays and IntStream.range split evenly. LinkedList and Stream.iterate split badly.
    2. The subranges become ForkJoin tasks, processed by the workers with work stealing.
    3. The partial results are combined (the reduce or collector combiner).
    4. Order-sensitive operations (forEachOrdered, limit on ordered streams, findFirst) add coordination costs.

Common trap: all parallel streams, and CompletableFuture.*Async calls without an executor, share the same common pool. Blocking I/O inside them starves everything else in the JVM.

Q14. Explain thread-pool internals, and the parallel stream fork-join behaviour, in one picture. (The DXB rapid-fire version)

Short answer:

  • Thread pool (ThreadPoolExecutor): a set of Worker threads, and one shared BlockingQueue. Tasks follow core → queue → max → reject. Workers loop on getTask() (take or a timed poll for the keep-alive), run the task, and repeat.
  • ForkJoinPool: one deque per worker, work stealing, and helping joins. Parallel streams use the common pool: split with a Spliterator → fork the subtasks → combine with the reducers.

Pick: ThreadPoolExecutor for independent jobs, with bounded queues and back-pressure. ForkJoin for recursive CPU work. Virtual threads for massive numbers of blocking I/O tasks.

Follow-up questions this topic invites — and their answers

Q: How do you size a thread pool? A: For CPU-bound work, about the number of cores (N or N+1). For blocking I/O, threads ≈ cores × (1 + wait time / compute time) (Goetz's formula), capped by downstream capacity (database pool size, rate limits). Better still, use virtual threads for I/O-bound work, and limit concurrency with semaphores or bulkheads instead of pool sizes.

Q: What does CallerRunsPolicy do, and why is it useful? A: When the pool is saturated, the submitting thread runs the task itself. That naturally slows the producer down (back-pressure), and nothing is dropped. The caveat is that it can block request threads, or event loops. The other policies are AbortPolicy (the default; it throws), DiscardPolicy and DiscardOldestPolicy.

Q: Why should thread pools have named threads? A: Thread dumps, logs and profilers become readable (pricing-7 instead of pool-3-thread-7). Use a ThreadFactory (Thread.ofPlatform().name("pricing-", 0).factory(), or Guava's ThreadFactoryBuilder), and set an uncaught-exception handler too.

Q: What is ScheduledThreadPoolExecutor's pitfall with exceptions? A: If a periodic task (scheduleAtFixedRate) throws, all future runs are silently cancelled, and the exception sits in the unobserved ScheduledFuture. Wrap the task body in try/catch and log inside it.

Previous

Concurrent Collections, Queues & Lock-Free Structures — Interview Questions

Next

Locks, Atomics, CAS & Synchronizers — Interview Questions

AI Tutor

Lesson: Threads, Executors & ForkJoin Internals — Interview Questions

Quick actions

AI responses can be inaccurate. Verify critical information.