Tuesday, July 22, 2025

• Multithreading, Concurrency (Executors, ForkJoin, CompletableFuture)

 

๐Ÿงต 1. Core Concepts of Multithreading

ConceptExplanation
ThreadSmallest unit of execution; runs independently.
Runnable / CallableRepresent units of work (Callable returns a result).
Thread SafetyMultiple threads safely accessing shared data (e.g., using synchronized, locks, atomics).
Race ConditionOccurs when threads access shared data without proper synchronization.
DeadlockTwo or more threads waiting on each other to release locks.

⚙️ 2. Executors Framework

Introduced in Java 5 to manage thread creation and pooling efficiently.

Executors (Thread Pools)

๐Ÿ”น Why Use Executors?

  • Creating threads manually is costly and inefficient.

  • Executors manage a pool of threads.

  • Avoids thread exhaustion & allows task reuse.

๐Ÿ”น Types of Executors

ExecutorService executor = Executors.newFixedThreadPool(4); // 4 worker threads executor.submit(() -> { System.out.println("Running in: " + Thread.currentThread().getName()); }); executor.shutdown();

ExecutorService Variants

ExecutorDescription
newFixedThreadPool(n)Fixed number of threads
newCachedThreadPool()Creates new threads as needed
newSingleThreadExecutor()Single-threaded executor
newScheduledThreadPool(n)For scheduled/periodic tasks

๐Ÿง  Use Future<T> for getting return values from tasks.

 

TypeBest Use Case
newFixedThreadPool(n)Predictable concurrency, limited threads
newCachedThreadPool()Burst loads, short-lived tasks
newSingleThreadExecutor()Sequential task execution
newScheduledThreadPool(n)Scheduled/periodic tasks

Interview Answer: “We used a fixed thread pool for CPU-bound tasks to avoid excessive context switching.”


๐Ÿง  3. ForkJoinPool (Java 7+)

Used for parallelism—divides tasks into smaller subtasks (divide-and-conquer).

๐Ÿ“ฆ Core Classes:

  • ForkJoinPool: Specialized thread pool to execute fork/join tasks.

  • RecursiveTask<V>: Used when tasks return a result.

  • RecursiveAction: Used when tasks do not return a result.

 Used for parallelism — especially in recursive tasks like:

  • Sorting

  • File searches

  • Data crunching

๐Ÿ”น Based on work stealing algorithm:

  • Idle threads steal tasks from busy ones

ForkJoinPool pool = new ForkJoinPool(); pool.invoke(new RecursiveTask<Integer>() { /*...*/ });
FeatureBenefit
Work-stealingIdle threads can "steal" tasks from others—boosts efficiency.
RecursiveTask / RecursiveActionHandles subtasks with/without results.

Interview Answer: “We parallelized complex data transformations with ForkJoinPool, which significantly reduced latency due to work-stealing.”


๐Ÿ”ฎ 4. CompletableFuture (Java 8+)

Asynchronous programming without blocking threads.

 Introduced in Java 8 for async programming.

  • Chain operations with callbacks (thenApply, thenAccept)

  • Combine multiple futures

  • Handle exceptions (exceptionally, handle)

CompletableFuture.supplyAsync(() -> fetchData()) .thenApply(data -> process(data)) .thenAccept(result -> save(result));


๐Ÿ”น Exception Handling

CompletableFuture<String> cf = CompletableFuture.supplyAsync(() -> { throw new RuntimeException("Oops"); }).exceptionally(ex -> "Default Value"); System.out.println(cf.get()); // Default Value

๐Ÿ” Use Cases

  • Async I/O and microservice orchestration

  • Parallel API calls

  • Composing dependent tasks without blocking

MethodPurpose
runAsync(Runnable)Runs a task asynchronously without return
supplyAsync(Supplier<T>)Runs task and returns a result
thenApply(Function<T,R>)Transforms result of future
thenAccept(Consumer<T>)Consumes result, returns void
thenCompose(Function)Flattens chained futures (dependent tasks)
thenCombine(future, BiFunction)Combines two futures in parallel
exceptionally(Function<Throwable,T>)Error recovery
handle(BiFunction<T, Throwable, R>)Handle success and error
allOf(f1, f2, …)Waits for all futures to complete
anyOf(f1, f2, …)Returns first completed future
join()Blocks and waits (like get() but unchecked)

Interview Answer: “We used CompletableFuture to orchestrate three async microservice calls and combine their results with thenCombine, reducing response time from 2s to under 800ms.”


⚠️ 5. Best Practices

PracticeBenefit
Use Executors, not raw ThreadScalable, better resource control
Use Thread-safe collectionsAvoid ConcurrentModificationException
Prefer immutable objectsReduce need for synchronization
Monitor thread poolsDetect bottlenecks (ThreadPoolExecutor#getActiveCount)
Avoid blocking in async flowsUse thenCompose instead of join()

✅ Pro Interview Template

"I chose CompletableFuture because we needed non-blocking orchestration of three APIs. It fits well into our microservices architecture and allows us to write readable, async code using thenApply and thenCombine, instead of managing threads manually. This keeps our service responsive and scalable."


Thread Management (ExecutorService)

By default, CompletableFuture uses the ForkJoinPool.commonPool(), but you can provide a custom thread pool:

ExecutorService executor = Executors.newFixedThreadPool(4); CompletableFuture.supplyAsync(() -> loadData(), executor);

Best Practices

PracticeWhy it Matters
Use ExecutorServiceManages thread pooling safely
Always shutdown() executorsPrevent resource leaks
Prefer Callable over RunnableAllows exception handling and result return
Use CompletableFuture for async chainsCleaner and non-blocking code
Use ForkJoinPool for recursive divide-and-conquerBetter performance for large tasks

⚠️  Common Pitfalls

PitfallExplanation
Blocking on .get() in main threadKills async benefit; prefer then* chains
Forgetting to shutdown executorsCauses app to hang
Using too many threadsThread contention, memory pressure
Not handling exceptions in async tasksCan silently fail or break chains

 

Difference between synchronized and ReentrantLock

✅ 1. synchronized (Intrinsic Lock)

๐Ÿ”น What is it?

A language-level keyword that provides mutual exclusion using the intrinsic lock (monitor) of an object.

๐Ÿ”ง Syntax:

synchronized(this) {
// critical section }

✅ Features:

  • Simple to use

  • Automatically releases the lock (even on exceptions)

  • Synchronized blocks/methods are reentrant by default

❌ Limitations:

  • No try-lock capability (you must wait)

  • No timeout or interrupt handling

  • Less flexible (can’t check lock status or manage fairness)


✅ 2. ReentrantLock (Explicit Lock)

๐Ÿ”น What is it?

A class from java.util.concurrent.locks package that gives fine-grained control over locking.

๐Ÿ”ง Syntax:

Lock lock = new ReentrantLock();
lock.lock(); try { // critical section } finally { lock.unlock(); // MUST release manually }

✅ Features:

  • Try-locking: tryLock() to attempt without blocking

  • Timeouts: tryLock(timeout, unit)

  • Interruptible: lockInterruptibly()

  • Fairness: Option to grant locks in FIFO order

  • Condition variables: Like wait()/notify() but more powerful

๐ŸŽฏ When to Use What?

Use CaseRecommendation
Simple mutual exclusionsynchronized
Need to try-lock or avoid blockingReentrantLock
Require fairness (FIFO lock order)ReentrantLock
Need multiple condition variablesReentrantLock
Short critical section, low contentionsynchronized

 difference between Runnable and Callable?

✅ Runnable vs Callable in Java

FeatureRunnableCallable<V>
Packagejava.langjava.util.concurrent
Return valueNo return value (void)Returns a result (V)
Can throw exceptionCannot throw checked exceptionsCan throw checked exceptions
Method to overriderun()call()
Submit viaThread or ExecutorServiceOnly ExecutorService
Used with Future?No (unless wrapped)Yes, returns Future<V>

๐Ÿ”ง Code Example: Runnable

Runnable task = () -> { System.out.println("Running task..."); }; Thread thread = new Thread(task); thread.start();
  • Does not return a result

  • Cannot throw checked exceptions

  • Executed via Thread or ExecutorService


๐Ÿ”ง Code Example: Callable

Callable<String> task = () -> { Thread.sleep(1000); return "Task result"; }; ExecutorService executor = Executors.newSingleThreadExecutor(); Future<String> future = executor.submit(task); String result = future.get(); // waits for result System.out.println(result); // Task result executor.shutdown();
  • Returns a result

  • Can throw checked exceptions

  • Executed using ExecutorService only

๐Ÿง  Interview Tip

“Use Runnable when no result or exception is needed. Use Callable when the task needs to return a value or might throw a checked exception. In production-grade concurrent apps, Callable with ExecutorService and Future provides better control.”


 

✅ Stream vs ParallelStream in Java

Featurestream()parallelStream()
ExecutionSequential (one thread)Parallel (multiple threads from ForkJoinPool)
PerformanceBetter for small/IO-bound tasksBetter for large/CPU-bound tasks
ThreadingSingle-threadedMulti-threaded (ForkJoinPool.commonPool)
OrderingPreserves order by defaultMay not preserve order
SuitabilitySimpler, more predictableRequires thread-safety and careful design

๐Ÿ” Example: stream()

List<String> names = List.of("Alice", "Bob", "Charlie"); names.stream() .map(String::toUpperCase) .forEach(System.out::println);
  • Runs sequentially from start to finish.

  • Easier to debug and reason about.


⚡ Example: parallelStream()

List<String> names = List.of("Alice", "Bob", "Charlie"); names.parallelStream() .map(String::toUpperCase) .forEach(System.out::println);
  • Splits data and runs parts concurrently using multiple threads.

  • Faster for large data sets — but only if thread-safe and no shared state.


No comments:

Post a Comment