| newFixedThreadPool(n) | n threads, unbounded queue; tasks wait if all busy |
| newCachedThreadPool() | Unlimited threads (created on demand); idle 60s then removed |
| newSingleThreadExecutor() | 1 thread; tasks execute sequentially in order |
| newScheduledThreadPool(n) | n threads; supports delay and periodic scheduling |
| newVirtualThreadPerTaskExecutor() | Java 21: one virtual thread per task |
| ForkJoinPool.commonPool() | Shared work-stealing pool; used by parallel streams |
Run immediately
Add to queue
RejectionHandler
ExecutorService & ThreadPoolExecutor
Q1. What is ExecutorService and why use it instead of raw Thread?
A: ExecutorService manages a pool of threads and provides APIs for task submission, lifecycle management, and result retrieval. Advantages over raw Thread: thread reuse (no creation overhead per task), bounded thread count, task queuing, graceful shutdown, Future/Callable support for return values, exception handling. Should always use ExecutorService or virtual threads instead of new Thread() in production.
Q2. What are the main Executors factory methods?
A: Executors.newFixedThreadPool(n): n core threads, n max threads, unbounded LinkedBlockingQueue. newCachedThreadPool(): 0 core, Integer.MAX_VALUE max, SynchronousQueue (no buffering); threads removed after 60s idle. newSingleThreadExecutor(): 1 thread, tasks ordered. newScheduledThreadPool(n): for delayed/periodic tasks. newVirtualThreadPerTaskExecutor() (Java 21): each task gets a new virtual thread.
Q3. What are the risks of newCachedThreadPool()?
A: Unlimited threads (max = Integer.MAX_VALUE). Under high load, creates thousands of threads — OutOfMemoryError "unable to create native thread". Use only for short-lived tasks with low concurrency. For production servers, prefer newFixedThreadPool or custom ThreadPoolExecutor with bounded queue and max thread count.
Q4. What are the risks of newFixedThreadPool()?
A: Unbounded task queue (LinkedBlockingQueue with Integer.MAX_VALUE capacity). Under sustained overload, tasks queue indefinitely — heap memory exhaustion (OutOfMemoryError). Use custom ThreadPoolExecutor with bounded queue + rejection handler instead for production systems.
Q5. How do you create a custom ThreadPoolExecutor?
ThreadPoolExecutor executor = new ThreadPoolExecutor(
4, // corePoolSize
8, // maximumPoolSize
60L, TimeUnit.SECONDS, // keepAliveTime for non-core threads
new ArrayBlockingQueue<>(100), // bounded work queue
new CustomThreadFactory(), // thread naming
new ThreadPoolExecutor.CallerRunsPolicy() // rejection policy
);
Q6. What are the ThreadPoolExecutor parameters?
A: 1) corePoolSize: minimum threads kept alive (even idle). 2) maximumPoolSize: max threads allowed. 3) keepAliveTime: idle non-core thread expiry time. 4) workQueue: queue for waiting tasks (ArrayBlockingQueue, LinkedBlockingQueue, SynchronousQueue, PriorityBlockingQueue). 5) threadFactory: creates threads (for naming/daemon). 6) handler: RejectedExecutionHandler when queue full + max threads reached.
Q7. What are the rejection policies in ThreadPoolExecutor?
A: AbortPolicy (default): throws RejectedExecutionException. CallerRunsPolicy: caller thread runs the task (slows producer, provides back-pressure). DiscardPolicy: silently drops new task. DiscardOldestPolicy: drops oldest queued task, re-submits new one. CallerRunsPolicy is best for most production use — prevents queue overflow and naturally throttles producers.
Q8. When does ThreadPoolExecutor create new threads?
A: Counter-intuitive algorithm: 1) If running threads < corePoolSize, create new thread (even if idle threads exist). 2) If running threads >= corePoolSize, add task to queue. 3) If queue full AND running threads < maximumPoolSize, create new thread. 4) If queue full AND at max threads, invoke rejection handler. Non-core threads are created ONLY after queue is full.
Q9. What is the difference between submit() and execute()?
A: execute(Runnable): submits Runnable, no return value, exceptions propagate to UncaughtExceptionHandler. submit(Runnable): returns Future<?> (null result), exceptions stored in Future — must call future.get() to see them. submit(Callable): returns Future<V> with result. submit() wraps exceptions; execute() propagates directly. Always use submit() to capture exceptions.
ExecutorService exec = Executors.newFixedThreadPool(4);
// execute: fire and forget
exec.execute(() -> riskyTask());
// submit: capture result or exception
Future<String> f = exec.submit(() -> { return compute(); });
try {
String result = f.get(5, TimeUnit.SECONDS);
} catch (ExecutionException e) {
// e.getCause() is the original exception from the task
}
Q10. What is invokeAll() and invokeAny()?
A: invokeAll(tasks): submits all tasks, blocks until ALL complete, returns List<Future>. invokeAny(tasks): submits all, blocks until FIRST successful completion, cancels remaining — returns result directly (no Future). invokeAll() with timeout cancels tasks not finished in time. Use invokeAny() for "fastest result wins" patterns (parallel search/query).
List<Callable<String>> tasks = List.of(() -> search1(), () -> search2(), () -> search3());
String fastest = executor.invokeAny(tasks); // returns first success
List<Future<String>> all = executor.invokeAll(tasks); // wait for all
Q11. What is ScheduledExecutorService?
A: ScheduledExecutorService extends ExecutorService for time-based scheduling. schedule(task, delay, unit): run once after delay. scheduleAtFixedRate(task, initialDelay, period, unit): run repeatedly at fixed rate (next start = last start + period; if task takes longer, starts immediately after). scheduleWithFixedDelay(task, initialDelay, delay, unit): fixed delay between end of one and start of next.
ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(2);
// Run after 5 seconds:
scheduler.schedule(() -> cleanup(), 5, TimeUnit.SECONDS);
// Every 10 seconds starting after 1 second:
scheduler.scheduleAtFixedRate(() -> healthCheck(), 1, 10, TimeUnit.SECONDS);
Q12. What is the difference between scheduleAtFixedRate and scheduleWithFixedDelay?
A: scheduleAtFixedRate: starts next execution at (start + period) regardless of how long the task took. If task takes longer than period, executions queue up (may overlap semantically but not actual overlap — next starts immediately when previous finishes). scheduleWithFixedDelay: waits for task to finish, THEN waits delay before starting next — guarantees gap between executions. Use FixedDelay when tasks must not overlap; FixedRate when execution must happen at precise intervals.
Q13. How do you properly shut down an ExecutorService?
void shutdown(ExecutorService executor) {
executor.shutdown(); // reject new tasks, let existing finish
try {
if (!executor.awaitTermination(30, TimeUnit.SECONDS)) {
executor.shutdownNow(); // interrupt running tasks
if (!executor.awaitTermination(30, TimeUnit.SECONDS))
log.error("Executor pool did not terminate");
}
} catch (InterruptedException e) {
executor.shutdownNow();
Thread.currentThread().interrupt();
}
}
Q14. What is the work queue's impact on thread pool behavior?
A: ArrayBlockingQueue(n): bounded, best for production — back-pressure when full. LinkedBlockingQueue: unbounded (or bounded with capacity) — default in newFixedThreadPool(). SynchronousQueue: no buffering, every submit must have a free thread (or creates new one) — used in newCachedThreadPool(). PriorityBlockingQueue: priority-based ordering — tasks implement Comparable.
Q15. What is the ThreadPoolExecutor.prestartAllCoreThreads() method?
A: Normally, core threads are created lazily on first task. prestartAllCoreThreads() starts all core threads immediately (or prestartCoreThread() for one). Use when you want the pool ready before first request arrives (reduce latency for early requests in web servers).
CompletableFuture Interview Questions
Q16. What is CompletableFuture and why was it introduced?
A: CompletableFuture<T> (Java 8) is a Future that can be manually completed and supports non-blocking callbacks and composition. Limitations of Future: get() is blocking, no callbacks, no composition, can't combine multiple futures, no exception recovery. CompletableFuture solves all of these with a fluent API for async pipelines.
Q17. How do you create a CompletableFuture?
// Async computation (runs in ForkJoinPool.commonPool()):
CompletableFuture<String> cf1 = CompletableFuture.supplyAsync(() -> fetchData());
// With custom executor:
CompletableFuture<String> cf2 = CompletableFuture.supplyAsync(() -> fetchData(), myExecutor);
// No return value:
CompletableFuture<Void> cf3 = CompletableFuture.runAsync(() -> process());
// Already completed:
CompletableFuture<String> cf4 = CompletableFuture.completedFuture("done");
Q18. What is the difference between thenApply, thenAccept, and thenRun?
A: thenApply(Function): transforms result, returns CompletableFuture<R> — chain transformations. thenAccept(Consumer): consumes result, returns CompletableFuture<Void> — side effects with result. thenRun(Runnable): runs after completion, no access to result, returns CompletableFuture<Void> — trigger side effects without result. All three run after the previous stage completes.
CompletableFuture.supplyAsync(() -> "hello")
.thenApply(s -> s.toUpperCase()) // "HELLO"
.thenApply(s -> "Result: " + s) // "Result: HELLO"
.thenAccept(System.out::println) // prints, returns Void
.thenRun(() -> System.out.println("done")); // no result access
Q19. What is thenCompose vs thenApply?
A: thenApply(Function<T,R>): wraps result in CompletableFuture — if function returns CF, you get CF<CF> (nested). thenCompose(Function<T, CF<R>>): flattens — use when the function itself returns a CF (async operation). Same distinction as Optional.map vs flatMap.
CompletableFuture<String> userId = CompletableFuture.supplyAsync(() -> "user-1");
// Wrong (double-wrapped):
CompletableFuture<CompletableFuture<User>> bad = userId.thenApply(id -> fetchUser(id));
// Correct:
CompletableFuture<User> good = userId.thenCompose(id -> fetchUser(id));
Q20. How do you combine two CompletableFutures?
A: thenCombine(cf2, BiFunction): both CFs run concurrently, combine results when both complete. thenAcceptBoth(cf2, BiConsumer): both run, consume both results (void). runAfterBoth(cf2, Runnable): both finish, then run action. allOf(cf1, cf2, ...): wait for ALL to complete (returns Void). anyOf(cf1, cf2, ...): complete when FIRST one finishes.
CompletableFuture<User> userCF = fetchUserAsync(id);
CompletableFuture<Order> orderCF = fetchOrderAsync(id);
// Run in parallel, combine when both done:
CompletableFuture<Dashboard> dashboard = userCF.thenCombine(orderCF,
(user, order) -> new Dashboard(user, order));
// Wait for all, then process:
CompletableFuture.allOf(cf1, cf2, cf3)
.thenRun(() -> System.out.println("All done"));
Q21. How do you handle exceptions in CompletableFuture?
A: exceptionally(Function<Throwable, T>): provides fallback value on exception — skipped if no exception. handle(BiFunction<T, Throwable, R>): always runs, receives result (or null on exception) and exception (or null on success) — can recover or re-throw. whenComplete(BiConsumer): always runs for side effects, does NOT transform result. exceptionallyAsync for async fallback.
CompletableFuture.supplyAsync(() -> riskyOperation())
.exceptionally(ex -> {
log.error("Failed", ex);
return "fallback"; // recovery value
});
// handle - always runs:
.handle((result, ex) -> {
if (ex != null) return "error: " + ex.getMessage();
return result.toUpperCase();
});
Q22. What thread runs CompletableFuture callbacks?
A: thenApply/thenAccept/thenRun (no "Async" suffix): run in the thread that completed the previous stage (or current thread if already completed). thenApplyAsync/thenAcceptAsync/thenRunAsync: run in ForkJoinPool.commonPool() (or provided executor). For callbacks doing I/O or heavy work, always use Async variants with a dedicated executor to avoid blocking commonPool threads.
Q23. What is CompletableFuture.allOf() and how do you get results?
List<CompletableFuture<String>> futures = List.of(cf1, cf2, cf3);
CompletableFuture<Void> all = CompletableFuture.allOf(
futures.toArray(new CompletableFuture[0]));
// allOf returns Void — to get results, chain thenApply:
CompletableFuture<List<String>> results = all.thenApply(v ->
futures.stream().map(CompletableFuture::join).collect(java.util.stream.Collectors.toList()));
Q24. What is the difference between get() and join() on CompletableFuture?
A: get(): throws InterruptedException (checked) and ExecutionException (checked, wraps actual exception). join(): throws CompletionException (unchecked, wraps actual exception) — no checked exception handling needed. Prefer join() in thenApply chains; use get() when you need to handle interruption explicitly.
Q25. How do you implement a timeout for CompletableFuture?
// Java 9+: orTimeout and completeOnTimeout
CompletableFuture<String> cf = fetchAsync()
.orTimeout(5, TimeUnit.SECONDS); // completes exceptionally with TimeoutException
CompletableFuture<String> cf2 = fetchAsync()
.completeOnTimeout("fallback", 5, TimeUnit.SECONDS); // complete with value on timeout
// Java 8 approach:
ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
scheduler.schedule(() -> cf.cancel(true), 5, TimeUnit.SECONDS);
Q26. What is CompletableFuture.failedFuture() in Java 9?
A: CompletableFuture.failedFuture(ex) creates an already-failed CompletableFuture with the given exception. Useful in tests or for returning pre-completed failures without async execution. Complement of completedFuture(value).
Q27. How do you cancel a CompletableFuture?
A: cf.cancel(mayInterruptIfRunning) — if not yet complete, completes the CF with CancellationException. The mayInterruptIfRunning parameter affects the underlying thread (if running in a pool) — but CompletableFuture doesn't have direct access to interrupt the thread, so true/false may have limited effect for CF (unlike Future from ExecutorService). Once cancelled, exceptionally/handle chains can catch CancellationException.
Q28. What is thenCombine vs runAfterBoth vs thenAcceptBoth?
| Method | Input | Returns |
|---|---|---|
| thenCombine | BiFunction (both results) | CF<R> — transformed |
| thenAcceptBoth | BiConsumer (both results) | CF<Void> — side effect |
| runAfterBoth | Runnable (no results) | CF<Void> — trigger |
Q29. What is applyToEither vs acceptEither vs runAfterEither?
A: Similar to combined methods but triggered by EITHER future completing (whichever first). applyToEither: transform whichever result comes first. acceptEither: consume first result (side effect). runAfterEither: run action when either completes (no result). Use for "race" patterns — fastest result wins.
Q30. What are the Thread pool sizing guidelines for CompletableFuture?
A: Default: ForkJoinPool.commonPool() (n-1 CPU threads) — good for CPU-bound work. For I/O-bound async tasks: use a separate Executor passed to supplyAsync/thenApplyAsync. Never use commonPool for blocking I/O — starves parallel streams and other CF chains. Rule: one pool per "type" of work (DB pool, HTTP pool, CPU pool).
ExecutorService ioPool = Executors.newFixedThreadPool(50);
CompletableFuture.supplyAsync(() -> callExternalApi(), ioPool)
.thenApplyAsync(result -> transform(result)) // CPU work on commonPool
.thenAcceptAsync(r -> saveToDb(r), ioPool); // DB write on ioPool
Q31. What is the difference between anyOf and firstSuccessOf pattern?
A: anyOf returns when ANY future completes — including exceptionally. There's no built-in firstSuccessOf. To get first SUCCESS: create CFs, use allOf-style with AtomicBoolean, or use thenAccept to complete a separate CompletableFuture only on success. In Java 21, structured concurrency provides a cleaner model.
Q32. How do you convert a blocking call to async with CompletableFuture?
// Wrap blocking call in supplyAsync with appropriate executor:
ExecutorService dbPool = Executors.newFixedThreadPool(10);
CompletableFuture<User> userFuture = CompletableFuture.supplyAsync(
() -> userRepository.findById(id), // blocking DB call
dbPool // runs on DB thread pool
);
// Chain non-blocking transformations:
userFuture.thenApply(user -> new UserDTO(user))
.thenAccept(dto -> response.setBody(dto));
Q33. What is CompletableFuture.delayedExecutor() in Java 9?
A: Creates an Executor that delays each task submission by a specified time. Useful for retry-with-delay patterns without a ScheduledExecutorService.
Executor delayed = CompletableFuture.delayedExecutor(1, TimeUnit.SECONDS);
CompletableFuture.supplyAsync(() -> retryOperation(), delayed);
Q34. How do you implement retry logic with CompletableFuture?
CompletableFuture<String> withRetry(Supplier<CompletableFuture<String>> op, int retries) {
return op.get().exceptionally(ex -> {
if (retries > 0) {
return withRetry(op, retries - 1).join(); // risky: recursive
}
throw new CompletionException(ex);
});
}
// Better: use a library (Resilience4j) for production retry logic
Q35. What happens if an exception occurs in thenApply and there's no exceptionally handler?
A: The exception propagates: the CompletableFuture transitions to failed state. All downstream thenApply/thenAccept/thenRun stages are skipped. If get() or join() is called, they throw ExecutionException/CompletionException. If no one calls get(), the exception is silently lost — always add exceptionally or handle at the end of a chain.
Q36. What is the difference between CompletableFuture and Spring WebFlux Mono?
A: CompletableFuture: eager (starts immediately), single value, callback-based. Mono (Reactor): lazy (only starts when subscribed), supports backpressure, richer operator set (retry, timeout, rate limiting built-in), better integration with reactive HTTP (WebFlux). CompletableFuture fits Spring MVC + virtual threads; Mono/Flux fits pure reactive (WebFlux) applications.
Q37. How does ForkJoinPool work?
A: ForkJoinPool uses work-stealing: each worker thread has its own deque. When a thread's deque is empty, it "steals" tasks from other threads' deques. Ideal for divide-and-conquer where tasks create subtasks (fork) and then wait for results (join). Worker count defaults to Runtime.getRuntime().availableProcessors() - 1 for commonPool.
Q38. What are the use cases for ForkJoinPool vs regular ThreadPool?
A: ForkJoinPool: recursive divide-and-conquer (merge sort, tree traversal), parallel stream operations, CPU-intensive computation. Regular ThreadPool (ExecutorService): independent tasks, I/O-bound work, web request handling, task queuing with rejection policies. Don't use ForkJoinPool for blocking I/O — its work-stealing relies on threads being active, not blocked.
Q39. What is a ManagedBlocker in ForkJoinPool?
A: ForkJoinPool.ManagedBlocker allows blocking operations in ForkJoin tasks without reducing parallelism. When a task blocks, ForkJoinPool creates a compensating thread to keep CPU busy. Use when you must do blocking work (I/O) inside ForkJoin tasks. Complex to implement — generally better to just avoid blocking in ForkJoin tasks.
Q40. What is the newVirtualThreadPerTaskExecutor() (Java 21)?
A: Creates an ExecutorService where each submitted task gets a new virtual thread. No thread pool — virtual threads are cheap enough to create one per task. Ideal for web servers: one virtual thread per HTTP request, blocking I/O is fine. Drop-in replacement for fixed thread pool in I/O-bound services.
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
List<Future<String>> futures = IntStream.range(0, 10_000)
.mapToObj(i -> executor.submit(() -> fetchFromApi(i)))
.collect(java.util.stream.Collectors.toList());
// 10,000 concurrent virtual threads — each may block on I/O
}
Q41. How does parallel stream use ForkJoinPool?
A: Parallel streams use ForkJoinPool.commonPool() by default. To use a custom pool: submit the stream operation as a Callable to a custom ForkJoinPool — it becomes the pool for that operation. Alternative (Java 19+): use Stream.parallel() with custom spliterator in a custom ForkJoinPool context.
ForkJoinPool customPool = new ForkJoinPool(8);
List<Result> results = customPool.submit(() ->
items.parallelStream().map(this::process).collect(java.util.stream.Collectors.toList())
).get();
Q42. What is the danger of using Thread.sleep() in an executor task?
A: sleep() holds the thread (wastes pool resources). In a 4-thread pool with tasks sleeping 10s, pool is blocked for 10s — queued tasks wait. With virtual threads: sleep() unmounts the virtual thread from carrier — carrier is free. Platform thread pools + sleep = bad; virtual threads + sleep = fine. Use ScheduledExecutorService for scheduled delays instead of sleep in pool tasks.
Q43. What is ExecutorCompletionService?
A: ExecutorCompletionService wraps an ExecutorService and adds a completion queue. As tasks complete, their futures are added to a BlockingQueue. take() returns the next completed future (in completion order, not submission order). Use when you want to process results as they complete (not waiting for all, not in submission order).
ExecutorCompletionService<String> ecs = new ExecutorCompletionService<>(executor);
tasks.forEach(ecs::submit);
for (int i = 0; i < tasks.size(); i++) {
String result = ecs.take().get(); // get results in completion order
process(result);
}
Q44. How do you limit concurrency with CompletableFuture?
// Use a bounded executor to limit concurrency:
ExecutorService limiter = Executors.newFixedThreadPool(5);
List<CompletableFuture<String>> futures = urls.stream()
.map(url -> CompletableFuture.supplyAsync(() -> fetch(url), limiter))
.collect(java.util.stream.Collectors.toList());
// Or use Semaphore inside the task for finer control:
Semaphore sem = new Semaphore(5);
urls.stream().map(url -> CompletableFuture.supplyAsync(() -> {
sem.acquire();
try { return fetch(url); } finally { sem.release(); }
}));
Q45. What is the ThreadLocal memory leak pattern in thread pools?
A: Thread pool threads live for the application lifetime. ThreadLocal values set during a request stay on the thread after the request ends. Next request on same thread sees stale data. Fix: always remove() in finally, or use request-scoped frameworks (Spring's RequestContextHolder). With virtual threads, every request has its own thread (discarded after) — ThreadLocal leaks disappear naturally.
Q46. What is the best practice for exception logging with CompletableFuture?
CompletableFuture.supplyAsync(() -> riskyWork())
.thenApply(r -> transform(r))
.whenComplete((result, ex) -> {
if (ex != null) log.error("Pipeline failed", ex); // always log
})
.exceptionally(ex -> defaultValue); // recovery
Q47. What is completedStage() and failedStage() in Java 9?
A: CompletableFuture.completedStage(value) returns a CompletionStage (not CompletableFuture) that is already completed — useful for API methods returning CompletionStage. CompletableFuture.failedStage(ex) returns a failed CompletionStage. The difference from completedFuture is the return type (interface vs class) — better for API design.
Q48. How do you implement a bulkhead pattern with ExecutorService?
A: Bulkhead: separate thread pools for different services — failure in one doesn't exhaust resources for others. Example: DB pool (10 threads), HTTP pool (20 threads), email pool (5 threads). If DB pool exhausted, HTTP calls continue. Combined with rejection policies (CallerRunsPolicy for back-pressure or fallbacks).
Q49. What is the effective use of CompletableFuture in Spring applications?
A: Spring @Async + CompletableFuture: annotate service methods with @Async returning CompletableFuture — Spring executes in async thread pool. Controller returns CompletableFuture — Spring MVC handles async response. With Spring WebFlux, prefer Mono/Flux. Spring Boot 3.2+: enable virtual threads globally for all @Async and HTTP handling.
Q50. How do you monitor thread pool metrics?
ThreadPoolExecutor tpe = (ThreadPoolExecutor) executor;
System.out.println("Active: " + tpe.getActiveCount());
System.out.println("Pool size: " + tpe.getPoolSize());
System.out.println("Queue size: " + tpe.getQueue().size());
System.out.println("Completed: " + tpe.getCompletedTaskCount());
// Expose via Micrometer in Spring Boot:
// new ExecutorServiceMetrics(executor, "myPool", List.of()).bindTo(registry);
Q51. What are the Java 21 structured concurrency benefits over CompletableFuture?
A: CompletableFuture issues: exception handling scattered, cancellation complex, stack traces shallow. Structured concurrency (JEP 453): all subtasks in a scope — if parent fails, subtasks cancelled automatically; if subtask fails, parent fails; all complete before scope exits. Clean stack traces. Simpler code for parallel fetch patterns without explicit allOf/exceptionally chains.
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
var user = scope.fork(() -> fetchUser(id));
var order = scope.fork(() -> fetchOrder(id));
scope.join().throwIfFailed(); // wait for both; throw if either failed
return new Dashboard(user.get(), order.get());
} // scope auto-closes: all subtasks done or cancelled
Q52. What is the difference between a task and a thread?
A: Thread: OS/JVM execution unit — expensive (1MB stack, OS scheduling). Task: unit of work (Runnable/Callable) — lightweight logical concept. Thread pools decouple task count from thread count: thousands of tasks execute on a small pool. Virtual threads bring task and thread closer — one virtual thread per task is now affordable.
Q53. What is the optimal number of threads for a web server?
A: Classic rule: threads = cores * (1 + blocking-time/cpu-time). For typical REST APIs (mostly DB I/O): 4-core server may use 40-100 threads. But this is empirical — measure with load tests. With virtual threads (Java 21): set unlimited (newVirtualThreadPerTaskExecutor) — JVM manages carrier threads (= N_cpu). Don't tune virtual thread counts; tune the carrier pool (usually defaults are correct).
Q54. How do you implement circuit breaker pattern with CompletableFuture?
A: Manual circuit breaker: AtomicInteger failureCount + AtomicBoolean open. In supplyAsync: if circuit open, return failed CF immediately; if closed, attempt call and update failure count. In production: use Resilience4j or Spring Cloud Circuit Breaker — they handle state machines, half-open state, metrics, and event listeners properly.
Q55. What is the CompletableFuture.complete() and completeExceptionally()?
A: complete(value): manually completes the CF with a value (if not already done). completeExceptionally(ex): completes with an exception. Both return true if this invocation caused completion, false if already complete. Used to bridge non-CF async APIs (callbacks, listeners) to CompletableFuture.
CompletableFuture<String> cf = new CompletableFuture<>();
// Bridge callback-based API:
asyncApiWithCallback(result -> cf.complete(result),
error -> cf.completeExceptionally(error));
return cf; // caller awaits this CF
Q56. What is the difference between Future.get() timeout and CompletableFuture.orTimeout()?
A: Future.get(timeout, unit): blocks for timeout, throws TimeoutException if exceeded — but the task keeps running in background. orTimeout(timeout, unit) (Java 9): completes the CF exceptionally with TimeoutException after timeout — downstream handlers are triggered. completeOnTimeout(value, timeout, unit): completes with a value instead of exception. orTimeout/completeOnTimeout are more composable — they fit the reactive pipeline model.
Q57. How does Spring Boot enable virtual threads for embedded Tomcat?
# application.properties (Spring Boot 3.2+, Java 21)
spring.threads.virtual.enabled=true
# Tomcat then uses virtual thread executor for request handling
# @Async also uses virtual threads automatically
Q58. What is the danger of blocking inside CompletableFuture.thenApply()?
A: thenApply runs on the completing thread (could be commonPool). Blocking in thenApply blocks that ForkJoin worker — reduces pool parallelism for ALL parallel streams and CF chains in the JVM. Always use thenApplyAsync(fn, ioPool) for blocking operations. Blocking in ForkJoin is the most common CF performance mistake.
Q59. What happens to queued tasks when shutdownNow() is called?
A: shutdownNow(): interrupts running threads (tries to cancel them), returns List<Runnable> of tasks that were waiting in the queue (never started). Caller can decide to run them, log them, or discard. No guarantee running tasks stop — they must respond to interruption. Tasks in CompletableFuture chains may not respond to interruption.
Q60. What is the relationship between ExecutorService and Spring @Async?
A: @Async runs annotated methods in a separate thread from Spring's task executor. Default: SimpleAsyncTaskExecutor (creates a new thread per call — no pool). Configure with @EnableAsync + @Bean TaskExecutor returning ThreadPoolTaskExecutor. Spring Boot 3.2+: configure with spring.threads.virtual.enabled=true for virtual threads. Always configure a bounded ThreadPoolTaskExecutor in production.
@Bean
public TaskExecutor taskExecutor() {
ThreadPoolTaskExecutor exec = new ThreadPoolTaskExecutor();
exec.setCorePoolSize(5);
exec.setMaxPoolSize(20);
exec.setQueueCapacity(100);
exec.setThreadNamePrefix("async-");
exec.initialize();
return exec;
}
Q61. What are RecursiveTask and RecursiveAction?
A: RecursiveTask<V>: ForkJoinTask subclass that returns a value from compute(). RecursiveAction: ForkJoinTask subclass with void compute() — no return. In compute(): check if small enough (base case), if not: fork subtasks (left.fork()), compute one subtask directly (right.compute()), join the forked subtask (left.join()), combine results. Always compute at least one subtask directly (no fork) to avoid all tasks being just scheduling overhead.
Q62. What is work stealing and when is it beneficial?
A: Work stealing: idle ForkJoin workers steal tasks from busy workers' queues. Benefits: automatic load balancing without central coordinator, excellent utilization for variable-length tasks. Ineffective for tasks of equal and short duration (stealing overhead dominates). Best for recursive divide-and-conquer where subtasks have unpredictable sizes.
Q63. How do you use CompletableFuture for fan-out/fan-in pattern?
// Fan-out: send requests to multiple services in parallel
List<CompletableFuture<ProductInfo>> futures = productIds.stream()
.map(id -> CompletableFuture.supplyAsync(() -> catalog.get(id), httpPool))
.collect(java.util.stream.Collectors.toList());
// Fan-in: collect all results
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]))
.thenApply(v -> futures.stream()
.map(CompletableFuture::join)
.collect(java.util.stream.Collectors.toList()))
.thenAccept(products -> response.setProducts(products));
Q64. How do you properly size a thread pool in production?
A: Step 1: Identify workload type (CPU vs I/O). Step 2: Start with formula (CPU: N_cpu; I/O: N_cpu * W where W = 1 + (wait/cpu ratio)). Step 3: Load test and measure: queue depth, active threads, response times. Step 4: Monitor in production with Micrometer — alert on queue depth spikes. Step 5: Adjust. Virtual threads (Java 21): skip sizing for I/O pools — use virtual thread executor.
Q65. What is the difference between a hot and cold Observable/CompletableFuture?
A: CompletableFuture is "hot" — computation starts immediately on supplyAsync, regardless of whether anyone calls get(). Reactor Mono is "cold" — computation starts only when subscribed. This affects caching (supplyAsync result is fixed once computed), sharing (same CF can be given to multiple callers), and cancellation (cancelling CF doesn't stop the async computation).
Q66. What is the best way to handle a mix of sync and async operations?
// Wrap sync in supplyAsync, then compose with async:
CompletableFuture.supplyAsync(() -> syncValidate(input)) // sync → async
.thenComposeAsync(validated -> asyncFetch(validated), ioPool) // async
.thenApply(data -> syncTransform(data)) // sync on completion thread
.thenAcceptAsync(result -> asyncSave(result), dbPool) // async write
Q67. What are common CompletableFuture anti-patterns?
A: 1) Calling join()/get() inside a CF chain (nested blocking — defeats async). 2) Blocking I/O in thenApply without Async (blocks pool thread). 3) Ignoring returned CF (fire and forget — exceptions lost). 4) Not handling exceptions (cf.thenApply().thenApply() with no exceptionally). 5) Using commonPool for blocking work. 6) Creating unbounded CFs without timeouts. 7) Not shutting down custom executors.
Q68. What is a CompletionStage vs CompletableFuture?
A: CompletionStage is the interface (thenApply, thenCompose, etc.). CompletableFuture implements CompletionStage + Future + provides complete/completeExceptionally. API methods should return CompletionStage for better encapsulation (hides complete/get). Internally use CompletableFuture, expose as CompletionStage.
Q69. How do you test async CompletableFuture code?
// Option 1: Use completedFuture for mocks:
when(service.fetchAsync(id)).thenReturn(CompletableFuture.completedFuture(data));
// Option 2: call get() in test (blocks until complete):
CompletableFuture<String> cf = service.doAsync();
String result = cf.get(5, TimeUnit.SECONDS);
assertThat(result).isEqualTo("expected");
// Option 3: Use Awaitility for more complex scenarios:
// await().atMost(5, SECONDS).until(() -> cf.isDone());
Q70. What is the key takeaway on choosing between Threads, Executors, CompletableFuture, and Virtual Threads?
A: Don't create raw Thread for every task — always use Executors or virtual threads. Executors (thread pools): for task isolation, bounded resources, scheduling. CompletableFuture: for composable async pipelines, fan-out/fan-in, non-blocking chains. Virtual threads (Java 21): for I/O-bound tasks at scale with simple synchronous code. Reactive (WebFlux/Mono): for fully non-blocking systems with backpressure. Mix: virtual threads for HTTP layer + CompletableFuture for async composition of business logic.
Post a Comment
Add