Skip to content

Latest commit

Β 

History

History
856 lines (582 loc) Β· 25 KB

File metadata and controls

856 lines (582 loc) Β· 25 KB

πŸ”Ή 1. What is ExecutorService?

  • ExecutorService is an interface in Java from the java.util.concurrent package.

  • It provides a way to manage and control multiple threads β€” including:

    • Submitting tasks to be executed
    • Managing their lifecycle
    • Shutting them down properly

πŸ”Ή 2. What is Executors.newFixedThreadPool(5)?

  • Executors is a utility class that provides factory methods to create different kinds of thread pools.

  • newFixedThreadPool(5) creates a thread pool with a fixed number (5) of threads.

  • This means:

    • At most 5 threads will be running concurrently.
    • If more than 5 tasks are submitted, the extra tasks will wait in a queue until a thread is free.

πŸ”Ή 3. What does this code do?

ExecutorService executorService = Executors.newFixedThreadPool(5);
  • It creates a thread pool of 5 threads.

  • You can now use executorService to submit tasks like this:

    executorService.submit(() -> {
        // some background task
        System.out.println("Task is running in a thread");
    });
  • It helps reuse threads and improves performance by avoiding the cost of creating new threads for every task.


πŸ”Ή 4. Real-world example

Imagine you want to send 100 emails:

  • Instead of creating 100 threads, you can use a fixed thread pool of 5 threads.
  • It will send emails in parallel (5 at a time), improving performance and saving system resources.

πŸ”Ή 5. Important: Always shut down the pool

Once you're done submitting tasks, shut it down properly:

executorService.shutdown();

Summary

Component Explanation
ExecutorService Interface to manage threads and tasks
Executors.newFixedThreadPool(5) Creates a pool with 5 threads
Purpose Efficiently manage multiple concurrent tasks with a limited number of threads

You can create threads manually in Java using new Thread(...). But using ExecutorService has many advantages, especially when working on real-world, scalable applications.

Let’s compare manual thread creation vs. ExecutorService, and explain why ExecutorService is preferred:


πŸ”΄ Manual Thread Creation

Thread t = new Thread(() -> {
    // do some work
});
t.start();

βœ… Pros:

  • Simple for one or two tasks.
  • Gives you full control over thread creation.

❌ Cons:

  1. No thread reuse: Each Thread is created fresh and destroyed after the task ends β†’ slow and memory-heavy.
  2. No task queue: You must manage your own queue if tasks arrive faster than threads can handle.
  3. No thread management: You can't easily manage a pool of threads or control how many run concurrently.
  4. No built-in error handling for task rejection or timeouts.
  5. Hard to scale: If you have hundreds or thousands of tasks, manual thread creation becomes a mess.

βœ… ExecutorService

ExecutorService pool = Executors.newFixedThreadPool(5);
pool.submit(() -> {
    // do some work
});

βœ… Pros:

  1. Thread pooling: Threads are reused β€” no overhead of constant creation/destruction.
  2. Automatic task queueing: Extra tasks wait in a queue.
  3. Better resource management: Limits how many threads run concurrently (e.g., to avoid CPU overload).
  4. Scalable: Easily handles many tasks.
  5. Graceful shutdown: You can shutdown() or awaitTermination() to properly manage application lifecycle.
  6. Future support: You can use Future to get results or handle exceptions from tasks.
  7. Scheduled tasks: You can schedule tasks with delays or periodically using ScheduledExecutorService.

βš–οΈ When to use what?

Use Case Recommended Approach
A few quick tasks Thread or Runnable is okay
Many tasks or long-running services ExecutorService is better
You need result/failure of task ExecutorService with Future
You need control over threads Use a custom ExecutorService or a ThreadPoolExecutor

🧠 Final Thought

While manual thread creation is fine for simple cases, ExecutorService gives you a production-ready, scalable, and robust way to manage concurrent tasks. It's widely used in web servers, background workers, scheduled tasks, etc.


The reason Java provides so many types of Executors is that different use cases need different threading strategies. There is no one-size-fits-all. Instead, Java gives you a toolbox β€” you pick the right one based on your needs.

πŸ”§ Overview of Executor Types (with When to Use)

Executor Thread Strategy When to Use
newFixedThreadPool(int n) Fixed number of reusable threads You have many short tasks and want to limit concurrency
newSingleThreadExecutor() Only one thread You want to run tasks one at a time, in order
newCachedThreadPool() Unlimited threads, reuses idle threads You have many short-lived tasks, possibly bursty
newScheduledThreadPool(int n) Fixed threads + supports delays You need to schedule tasks (like a cron job or timer)
newSingleThreadScheduledExecutor() One thread + scheduling support Same as above, but for sequential scheduled tasks
newWorkStealingPool() ForkJoinPool with multiple queues Best for many small parallel tasks (CPU-bound work)
newThreadPerTaskExecutor(ThreadFactory) Creates new thread for each task Avoid unless you want a thread per task (heavyweight)
newVirtualThreadPerTaskExecutor() (Java 21+) New virtual thread per task Great for high concurrency, like servers, light threads

πŸ” Which one should you use?

Let’s map them to real-world scenarios:


1. Executors.newFixedThreadPool(n)

βœ… Best for most common use cases

  • Reuse a fixed number of threads
  • CPU-bound or IO-bound tasks
  • You want to limit the number of concurrent threads

🧠 Use this when:

ExecutorService pool = Executors.newFixedThreadPool(10);

E.g., 10 threads to process user uploads.


2. Executors.newSingleThreadExecutor()

βœ… Ensures sequential execution

  • Tasks are queued and run one after another
  • Used when order matters

🧠 E.g., logging, file-writing in order.


3. Executors.newCachedThreadPool()

βœ… Dynamic scaling, unlimited threads

  • Great for bursty traffic, short-lived tasks
  • Idle threads are reused

❌ Risk: Can exhaust system resources if many long tasks pile up

🧠 E.g., Fire-and-forget tasks like email sending.


4. Executors.newScheduledThreadPool(n)

βœ… Use for repeated/scheduled tasks

  • Supports .schedule() and .scheduleAtFixedRate()

🧠 E.g., run DB cleanup every hour.


5. Executors.newSingleThreadScheduledExecutor()

βœ… Like above, but with just 1 thread

🧠 E.g., Schedule one task to run hourly in strict order


6. Executors.newWorkStealingPool()

βœ… Uses a ForkJoinPool, good for divide-and-conquer

  • Best for parallel computation
  • Tasks are split and stolen by idle threads

🧠 E.g., recursive algorithms, sorting, parallel tasks


7. Executors.newThreadPerTaskExecutor(ThreadFactory)

❌ Not commonly used β€” creates one thread per task

  • Very heavy if tasks are frequent
  • Used rarely (mostly for experimentation)

8. Executors.newVirtualThreadPerTaskExecutor() βœ… (Java 21+)

πŸ”₯ Next-gen concurrency: lightweight threads (Project Loom)

  • Thousands of virtual threads can run on few platform threads
  • Great for high-concurrency I/O-heavy apps

🧠 E.g., modern server handling thousands of connections.


πŸ’‘ Final Tips

Need Recommended
1 task at a time newSingleThreadExecutor()
Many tasks, limited concurrency newFixedThreadPool(n)
Short tasks, unpredictable load newCachedThreadPool()
Scheduled tasks newScheduledThreadPool()
CPU-intensive, divide-and-conquer newWorkStealingPool()
Modern Java, high concurrency newVirtualThreadPerTaskExecutor() (Java 21+)

The order of task execution in an ExecutorService depends on which type of executor you use.

Let’s break it down by executor type:


βœ… 1. newSingleThreadExecutor()

  • Tasks run in the order you submit them (FIFO – First In, First Out).
  • There is only one thread, so they can't run in parallel.

🧠 Guaranteed order.

ExecutorService executor = Executors.newSingleThreadExecutor();
executor.submit(() -> System.out.println("Task 1"));
executor.submit(() -> System.out.println("Task 2"));
executor.submit(() -> System.out.println("Task 3"));
// Output: Task 1 β†’ Task 2 β†’ Task 3

βœ… 2. newFixedThreadPool(n)

  • No guaranteed order.
  • Tasks are taken from a queue, but multiple threads pick them up, so the execution can be out of order.

🧠 Submission order β‰  execution order, especially if:

  • Tasks take different times.
  • Threads pick up tasks in a different order.
ExecutorService executor = Executors.newFixedThreadPool(2);
executor.submit(() -> System.out.println("Task 1"));
executor.submit(() -> System.out.println("Task 2"));
executor.submit(() -> System.out.println("Task 3"));
// Possible output: Task 2 β†’ Task 1 β†’ Task 3

βœ… 3. newCachedThreadPool()

  • Similar to FixedThreadPool β€” no guaranteed order.
  • Spawns new threads as needed, so task execution order can vary a lot.

βœ… 4. newScheduledThreadPool() or newSingleThreadScheduledExecutor()

  • If you schedule tasks with delays, execution depends on time, not submission order.

βœ… 5. newWorkStealingPool()

  • Uses multiple queues per thread, so task order is not guaranteed at all.
  • Focus is on throughput, not order.

βœ… 6. newVirtualThreadPerTaskExecutor() or newThreadPerTaskExecutor()

  • Creates a new thread per task β†’ no order guarantee.
  • Tasks run concurrently.

πŸ” Summary Table

Executor Type Execution Order
SingleThreadExecutor βœ… In order
FixedThreadPool ❌ Not guaranteed
CachedThreadPool ❌ Not guaranteed
ScheduledThreadPool ⏰ Based on delay/period
WorkStealingPool ❌ Not guaranteed
VirtualThreadPerTaskExecutor ❌ Not guaranteed
ThreadPerTaskExecutor ❌ Not guaranteed

βœ… If order matters, go with:

  • SingleThreadExecutor (for serial tasks)
  • Or use a blocking queue outside the pool and control the order manually.

When you submit a task to an ExecutorService, it doesn’t return the result of the task directly β€” it returns a Future object instead.

🧠 What is Future?

A Future<T> represents the result of an asynchronous computation. It acts like a promise that:

  • The task will complete
  • You can check if it's done
  • You can get the result when ready
  • You can cancel the task

πŸ“¦ How it works

ExecutorService executor = Executors.newFixedThreadPool(2);

Future<Integer> future = executor.submit(() -> {
    Thread.sleep(2000); // Simulate work
    return 42;
});

Now:

System.out.println("Task submitted...");
Integer result = future.get(); // This will block until result is ready
System.out.println("Result = " + result);

βœ… Key Methods in Future

Method What it does
get() Blocks and waits for the result
get(timeout, unit) Waits for result for given time, else throws TimeoutException
isDone() Returns true if task is finished
isCancelled() Returns true if task was cancelled
cancel(true) Attempts to cancel the task

πŸ§ͺ Example Usage

Future<String> future = executor.submit(() -> {
    Thread.sleep(3000);
    return "Hello from background!";
});

System.out.println("Doing other stuff...");

String result = future.get(); // Waits here if not done
System.out.println(result);

🧯 What if You Don’t Use Future?

You can use execute() instead of submit():

executor.execute(() -> System.out.println("Fire and forget task"));

But then:

  • You don’t get result
  • You can’t know if it succeeded or failed
  • You can’t cancel it

βœ… When to Use Future?

Use it when:

  • You want to get the result
  • You want to check or control task execution
  • You want to handle exceptions

🧠 Bonus: CompletableFuture (Java 8+)

Future is basic. For chaining, async workflows, etc., prefer CompletableFuture.


βœ… Part 1: What is CompletableFuture?

CompletableFuture<T> is a more powerful and flexible version of Future introduced in Java 8.

It allows you to:

  • Run tasks asynchronously
  • Chain tasks together
  • Handle results, errors, and exceptions
  • Write non-blocking code
  • Easily use functional programming

πŸ”§ Basic Example

CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
    // Runs in a background thread
    return "Hello";
});

To get the result:

String result = future.get();  // Blocks until result is ready

Or handle it without blocking:

future.thenAccept(result -> System.out.println("Result: " + result));

πŸ” Chaining Tasks

CompletableFuture.supplyAsync(() -> "Hello")
                 .thenApply(str -> str + " World")
                 .thenAccept(System.out::println); // Prints: Hello World

Each stage runs only after the previous one completes.


πŸš€ Parallel Execution and Combination

CompletableFuture<String> f1 = CompletableFuture.supplyAsync(() -> "A");
CompletableFuture<String> f2 = CompletableFuture.supplyAsync(() -> "B");

f1.thenCombine(f2, (a, b) -> a + b)
  .thenAccept(System.out::println); // Prints: AB

❌ Exception Handling

CompletableFuture.supplyAsync(() -> {
    if (true) throw new RuntimeException("Boom");
    return "Hello";
}).exceptionally(ex -> {
    System.out.println("Caught: " + ex.getMessage());
    return "Fallback";
});

βœ… Part 2: Difference Between Future and CompletableFuture

Feature Future CompletableFuture
Introduced in Java 5 Java 8
Result retrieval get() (blocks) get(), or non-blocking with then...()
Async execution Needs ExecutorService Built-in support via supplyAsync(), etc.
Task chaining ❌ Not possible βœ… Fluent chaining with thenApply, etc.
Combining multiple tasks ❌ No built-in support βœ… Easy with thenCombine, allOf, etc.
Error handling Manual via try-catch Built-in: exceptionally(), handle()
Non-blocking support ❌ Only blocking with get() βœ… Full support for non-blocking
Event-based callbacks ❌ Not available βœ… Supported
Completing manually ❌ No βœ… Can call complete(), completeExceptionally()

🧠 When to Use What?

  • βœ… Use Future for simple async tasks where you just need:

    • To submit
    • To wait for result
    • Maybe cancel it
  • βœ… Use CompletableFuture for:

    • Complex workflows
    • Chaining and combining tasks
    • Parallel execution
    • Better error handling
    • Non-blocking applications (e.g., web services)

Sure! Here's a complete example for each use case using CompletableFuture. These patterns are commonly used in real-world backend services like microservices or async APIs.


βœ… 1. Calling Multiple APIs in Parallel

CompletableFuture<String> api1 = CompletableFuture.supplyAsync(() -> {
    sleep(1000);
    return "API 1 result";
});

CompletableFuture<String> api2 = CompletableFuture.supplyAsync(() -> {
    sleep(2000);
    return "API 2 result";
});

CompletableFuture<String> combined = api1.thenCombine(api2, (res1, res2) -> res1 + " + " + res2);

System.out.println(combined.get()); // Output after 2 seconds: API 1 result + API 2 result

βœ… 2. Retry Logic with CompletableFuture

Since CompletableFuture has no direct retry method, we write a wrapper:

public static <T> CompletableFuture<T> retry(Supplier<T> task, int attempts) {
    return CompletableFuture.supplyAsync(() -> {
        while (true) {
            try {
                return task.get();
            } catch (Exception e) {
                if (--attempts == 0) throw new RuntimeException("All retries failed", e);
                System.out.println("Retrying...");
                sleep(500);
            }
        }
    });
}

// Usage:
CompletableFuture<String> retried = retry(() -> {
    if (Math.random() < 0.7) throw new RuntimeException("API failed");
    return "Success";
}, 3);

System.out.println(retried.get());

βœ… 3. Timeout with Fallback

CompletableFuture<String> slowApi = CompletableFuture.supplyAsync(() -> {
    sleep(3000);
    return "Slow API result";
});

// Timeout logic
CompletableFuture<String> withTimeout = slowApi
    .completeOnTimeout("Fallback value", 2, TimeUnit.SECONDS);

System.out.println(withTimeout.get()); // Will print "Fallback value" if API takes >2s

🧰 Helper Sleep Method (used in all examples)

private static void sleep(long ms) {
    try { Thread.sleep(ms); } catch (InterruptedException ignored) {}
}

Let's explore how to schedule tasks at a fixed rate or with a delay using ScheduledExecutorService.

This is useful when you want to:

  • Run a task repeatedly at regular intervals
  • Delay the execution of a task
  • Run periodic background jobs like health checks, log cleanup, etc.

βœ… Setup: Create a ScheduledExecutorService

ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);

βœ… Example 1: Schedule with Fixed Delay

scheduler.scheduleWithFixedDelay(() -> {
    System.out.println("Task with delay - " + System.currentTimeMillis());
    sleep(1000); // simulate work
}, 0, 2, TimeUnit.SECONDS);

πŸ” What it does:

  • Starts immediately (0s delay)
  • Waits 2 seconds after the task finishes before starting again

βœ… Example 2: Schedule at Fixed Rate

scheduler.scheduleAtFixedRate(() -> {
    System.out.println("Fixed rate task - " + System.currentTimeMillis());
}, 0, 3, TimeUnit.SECONDS);

πŸ” What it does:

  • Starts immediately
  • Then runs every 3 seconds, no matter how long the task takes
  • If the task takes longer than 3s, it tries to catch up

βœ… Example 3: One-time Delayed Task

scheduler.schedule(() -> {
    System.out.println("Run after 5 seconds delay");
}, 5, TimeUnit.SECONDS);

βœ… Example 4: Canceling the Task

ScheduledFuture<?> future = scheduler.scheduleAtFixedRate(() -> {
    System.out.println("Running task...");
}, 0, 2, TimeUnit.SECONDS);

// Cancel after 10 seconds
scheduler.schedule(() -> {
    System.out.println("Cancelling...");
    future.cancel(true);
    scheduler.shutdown();
}, 10, TimeUnit.SECONDS);

🧠 FixedRate vs FixedDelay

Feature scheduleAtFixedRate scheduleWithFixedDelay
Time between executions Fixed, regardless of task duration Waits a fixed delay after task finishes
Use when You want precise periodic execution You want fixed gap between runs
Risk Tasks may overlap or pile up if too slow Safer β€” always waits before next run

let’s go deeper into cron-like scheduling and real-world examples using ScheduledExecutorService.

βœ… 1. Cron-like Scheduling

Java itself does not have a built-in cron parser like Linux cron, but you can:

πŸ”Έ Option 1: Use ScheduledExecutorService for fixed-rate jobs

scheduler.scheduleAtFixedRate(() -> {
    System.out.println("Runs every 10 seconds like a cron job");
}, 0, 10, TimeUnit.SECONDS);

This is like a cron: */10 * * * * * (every 10 seconds)


πŸ”Έ Option 2: Use quartz library (for true cron expressions)

<!-- Add to pom.xml -->
<dependency>
  <groupId>org.quartz-scheduler</groupId>
  <artifactId>quartz</artifactId>
  <version>2.3.2</version>
</dependency>
JobDetail job = JobBuilder.newJob(MyJob.class)
        .withIdentity("cronJob")
        .build();

Trigger trigger = TriggerBuilder.newTrigger()
        .withSchedule(CronScheduleBuilder.cronSchedule("0/10 * * * * ?"))
        .build();

Scheduler scheduler = new StdSchedulerFactory().getScheduler();
scheduler.start();
scheduler.scheduleJob(job, trigger);

Where MyJob is:

public class MyJob implements Job {
    public void execute(JobExecutionContext context) {
        System.out.println("Quartz Cron Job Running at " + new Date());
    }
}

βœ… Use this when you need real cron expressions like:

  • 0 0/15 * * * ? β†’ every 15 minutes
  • 0 0 9 ? * MON-FRI β†’ 9 AM every weekday

βœ… 2. Real-World Examples


πŸ” A. Polling a Database Every 10 Seconds

scheduler.scheduleAtFixedRate(() -> {
    System.out.println("Checking DB for new records at " + new Date());
    // Simulate DB poll
    List<String> newRecords = fetchNewRecordsFromDB();
    newRecords.forEach(System.out::println);
}, 0, 10, TimeUnit.SECONDS);
private static List<String> fetchNewRecordsFromDB() {
    // Simulated data
    return List.of("Record 1", "Record 2");
}

♻️ B. Retry Queue Example (with Exponential Backoff)

Suppose we retry failed messages with backoff:

public void retryWithBackoff(String message, int attempt) {
    long delay = (long) Math.pow(2, attempt); // 2^attempt seconds
    scheduler.schedule(() -> {
        try {
            sendToApi(message);
        } catch (Exception e) {
            if (attempt < 5) {
                retryWithBackoff(message, attempt + 1);
            } else {
                System.out.println("Max retries reached. Dropping message.");
            }
        }
    }, delay, TimeUnit.SECONDS);
}
public void sendToApi(String msg) {
    System.out.println("Sending: " + msg);
    if (Math.random() < 0.7) throw new RuntimeException("Failed!");
    System.out.println("Success: " + msg);
}

βœ… Summary

Goal Best Approach
Cron-like precision Use Quartz or Spring's @Scheduled(cron)
Polling DB/API Use scheduleAtFixedRate
Retry with backoff Use recursive scheduler.schedule()
Lightweight jobs Use ScheduledExecutorService